Skip to content

Commit

Permalink
create associated systems module (#1043)
Browse files Browse the repository at this point in the history
* create associated systems module

* created `AssociatedSystemsModule` which is sort of like a modernized version of the addressresolver for v3. You specify an implementation address, (which can be a *router* deployed by the deployer with cannon) and then it will deploy a proxy to officially connect it to the system. Supports doing the upgrades in the same call. also supports adding unmanaged contracts (ex. when we deploy to prod, using the old SNX contract)
* change deployer ability to *not* deploy proxies. This should be done with cannon now
* change deployer to take in `modules` argument (positional), which has the ability to specify which modules to deploy. This is used by cannon to build subsets of the module directory for the 4 different systems specified above.
* add `TokenModule` and `NftModule` to `core-modules` which implements everything needed for a basic token as the title implies
* remove checks for initialization modules (though not the initialization module itself), since initialization will be a much more customized process going forward and will be done by just implementing "isInitialized" for your module.

* lint

* add skipProxy option and remove unnecessary task name

* better logging for cli mismatch errors

* oops

* remove unnecessary log

* fix deployer

* fix pkglock

* fix deployer merge issues

* lint fix

* update pkglock

* update module tests

* add tests for associated systems manager and TokenModule and NftModule

* lint

* figured out why owner calls werent working

turns out the deployment was being overwritten with same modules. to solve,
deploy all systems to a new instance

thanks @leo

* fix from review

* remove rogue file

* fix cli-runner lint warnings

* remove proxyContract configuration from deployments util

Co-authored-by: Matías <mjlescano@protonmail.com>
  • Loading branch information
dbeal-eth and mjlescano committed Aug 4, 2022
1 parent 11967f7 commit 2bb404a
Show file tree
Hide file tree
Showing 52 changed files with 17,542 additions and 12,014 deletions.
28,195 changes: 16,451 additions & 11,744 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 12 additions & 10 deletions packages/cli/test/integration/helpers/cli-runner.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const assert = require('assert/strict');
const chalk = require('chalk');
const { spawn } = require('child_process');
const { setTimeout } = require('timers/promises');
const chalk = require('chalk');

// Set these to false on CI
const SHOW_CLI_OUTPUT = false;
Expand All @@ -12,7 +13,7 @@ const INTERACT_DELAY = 1000;
class CliRunner {
constructor() {}

async start() {
start() {
this.errors = [];
this.buffer = '';

Expand All @@ -38,27 +39,24 @@ class CliRunner {

this.buffer += str;
});

this.cliProcess.stderr.on('data', (data) => {
console.error(data.toString());

this.errors.push(data.toString());
});

return new Promise((resolve) => {
setTimeout(resolve, START_DELAY);
});
return setTimeout(START_DELAY);
}

async interact(cmd) {
interact(cmd) {
if (SHOW_CLI_INTERACTIONS) {
console.log(`CLI input: ${cmd}`);
}

this.cliProcess.stdin.write(cmd);

return new Promise((resolve) => {
setTimeout(resolve, INTERACT_DELAY);
});
return setTimeout(INTERACT_DELAY);
}

clear() {
Expand All @@ -68,7 +66,11 @@ class CliRunner {
printed(txt) {
const includes = this.buffer.includes(txt);
if (!includes) {
console.error(`CLI output was expected to include "${chalk.red(txt)}", but it does not.`);
console.error(
`CLI output should contain "${chalk.white(txt)}", but it was "${chalk.red(
this.buffer.toString()
)}".`
);
}

assert.ok(includes);
Expand Down
10 changes: 10 additions & 0 deletions packages/core-js/utils/ethers/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
function findEvent({ receipt, eventName, contract = undefined }) {
let events = receipt.events;

if (!receipt) {
throw new Error(`receipt when searching for event ${eventName} is null/undefined.`);
}

if (!receipt.logs) {
throw new Error(
`no logs found when searching for event ${eventName}. Did you actually pass a transaction receipt into findEvent?`
);
}

if (!events || (events.some((e) => e.event === undefined) && contract)) {
events = parseLogs({ contract, logs: receipt.logs });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title Module for managing snxUSD token as a Satellite
interface IAssociatedSystemsConsumerModule {
function getToken(bytes32 id) external view returns (address);

function getNft(bytes32 id) external view returns (address);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title Allows for the registration and tracking of auxillery contracts which also follow the proxy architecture
interface IAssociatedSystemsModule {
/// @notice create or initialize a new token
function initOrUpgradeToken(
bytes32 id,
string memory name,
string memory symbol,
uint8 decimals,
address impl
) external;

function initOrUpgradeNft(
bytes32 id,
string memory name,
string memory symbol,
string memory uri,
address impl
) external;

function registerUnmanagedSystem(bytes32 id, address endpoint) external;

function getAssociatedSystem(bytes32 id) external view returns (address proxy, bytes32 kind);
}
20 changes: 20 additions & 0 deletions packages/core-modules/contracts/interfaces/INftModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@synthetixio/core-contracts/contracts/interfaces/IERC721.sol";

/// @title NFT token identifying an Account
interface INftModule is IERC721 {
/// @notice returns if `initialize` has been called by the owner
function isInitialized() external returns (bool);

/// @notice allows owner to initialize the token after attaching a proxy
function initialize(
string memory tokenName,
string memory tokenSymbol,
string memory uri
) external;

/// @notice mints a new token (NFT) with the "requestedAccountId" id owned by "owner". It can ol=nly be called by the system
function mint(address owner, uint requestedAccountId) external;
}
30 changes: 30 additions & 0 deletions packages/core-modules/contracts/interfaces/ITokenModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@synthetixio/core-contracts/contracts/interfaces/IERC20.sol";

/// @title ERC20 token for snxUSD
interface ITokenModule is IERC20 {
/// @notice returns if `initialize` has been called by the owner
function isInitialized() external returns (bool);

/// @notice allows owner to initialize the token after attaching a proxy
function initialize(
string memory tokenName,
string memory tokenSymbol,
uint8 tokenDecimals
) external;

/// @notice mints token amount to "to" address
function mint(address to, uint amount) external;

/// @notice burns token amount from "to" address
function burn(address to, uint amount) external;

/// @notice sets token amount allowance to spender by "from" address
function setAllowance(
address from,
address spender,
uint amount
) external;
}
42 changes: 42 additions & 0 deletions packages/core-modules/contracts/mixins/AssociatedSystemsMixin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@synthetixio/core-contracts/contracts/errors/InitError.sol";
import "../storage/AssociatedSystemsStorage.sol";

import "../interfaces/ITokenModule.sol";
import "../interfaces/INftModule.sol";

contract AssociatedSystemsMixin is AssociatedSystemsStorage {
error MismatchAssociatedSystemKind(bytes32 expected, bytes32 actual);

bytes32 internal constant _KIND_ERC20 = "erc20";
bytes32 internal constant _KIND_ERC721 = "erc721";
bytes32 internal constant _KIND_UNMANAGED = "unmanaged";

function _getToken(bytes32 id) internal view returns (ITokenModule) {
_requireKind(id, _KIND_ERC20);
return ITokenModule(_associatedSystemsStore().satellites[id].proxy);
}

function _getNft(bytes32 id) internal view returns (INftModule) {
_requireKind(id, _KIND_ERC721);
return INftModule(_associatedSystemsStore().satellites[id].proxy);
}

function _requireKind(bytes32 id, bytes32 kind) internal view {
bytes32 actualKind = _associatedSystemsStore().satellites[id].kind;

if (actualKind != kind && actualKind != _KIND_UNMANAGED) {
revert MismatchAssociatedSystemKind(kind, actualKind);
}
}

modifier onlyIfAssociated(bytes32 id) {
if (address(_associatedSystemsStore().satellites[id].proxy) == address(0)) {
revert InitError.NotInitialized();
}

_;
}
}
105 changes: 105 additions & 0 deletions packages/core-modules/contracts/modules/AssociatedSystemsModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@synthetixio/core-contracts/contracts/ownership/OwnableMixin.sol";
import "@synthetixio/core-contracts/contracts/proxy/UUPSProxy.sol";
import "../interfaces/IAssociatedSystemsModule.sol";
import "../mixins/AssociatedSystemsMixin.sol";

import "@synthetixio/core-contracts/contracts/interfaces/IUUPSImplementation.sol";
import "../interfaces/IOwnerModule.sol";
import "../interfaces/ITokenModule.sol";
import "../interfaces/INftModule.sol";

contract AssociatedSystemsModule is IAssociatedSystemsModule, OwnableMixin, AssociatedSystemsMixin {
function initOrUpgradeToken(
bytes32 id,
string memory name,
string memory symbol,
uint8 decimals,
address impl
) external override onlyOwner {
AssociatedSystemsStore storage store = _associatedSystemsStore();

if (store.satellites[id].proxy != address(0)) {
_requireKind(id, _KIND_ERC20);

store.satellites[id].impl = impl;

address proxy = store.satellites[id].proxy;

// tell the associated proxy to upgrade to the new implementation
IUUPSImplementation(proxy).upgradeTo(impl);

_setAssociatedSystem(id, _KIND_ERC20, proxy, impl);
} else {
// create a new proxy and own it
address proxy = address(new UUPSProxy(impl));

IOwnerModule(proxy).initializeOwnerModule(address(this));
ITokenModule(proxy).initialize(name, symbol, decimals);

_setAssociatedSystem(id, _KIND_ERC20, proxy, impl);
}
}

function initOrUpgradeNft(
bytes32 id,
string memory name,
string memory symbol,
string memory uri,
address impl
) external override onlyOwner {
AssociatedSystemsStore storage store = _associatedSystemsStore();

if (store.satellites[id].proxy != address(0)) {
_requireKind(id, _KIND_ERC721);

address proxy = store.satellites[id].proxy;

// tell the associated proxy to upgrade to the new implementation
IUUPSImplementation(proxy).upgradeTo(impl);

_setAssociatedSystem(id, _KIND_ERC721, proxy, impl);
} else {
// create a new proxy and own it
address proxy = address(new UUPSProxy(impl));

IOwnerModule(proxy).initializeOwnerModule(address(this));
INftModule(proxy).initialize(name, symbol, uri);

_setAssociatedSystem(id, _KIND_ERC721, proxy, impl);
}
}

/**
* sets a token implementation without the corresponding upgrade functionality
* useful for adaptation of ex. old SNX token. The connected system does not need to be
*
* *NOTE:* the contract you are connecting should still be owned by your dao. The
* system is not expected to be able to do upgrades for you.
*/
function registerUnmanagedSystem(bytes32 id, address endpoint) external override onlyOwner {
// empty string require kind will make sure the system is either unregistered or already unmanaged
_requireKind(id, "");

_setAssociatedSystem(id, _KIND_UNMANAGED, endpoint, endpoint);
}

function _setAssociatedSystem(
bytes32 id,
bytes32 kind,
address proxy,
address impl
) internal {
_associatedSystemsStore().satellites[id] = AssociatedSystem(proxy, impl, kind);
emit AssociatedSystemSet(kind, id, proxy, impl);
}

function getAssociatedSystem(bytes32 id) external view override returns (address proxy, bytes32 kind) {
proxy = _associatedSystemsStore().satellites[id].proxy;
kind = _associatedSystemsStore().satellites[id].kind;
}

event AssociatedSystemSet(bytes32 indexed kind, bytes32 indexed id, address proxy, address impl);
}
47 changes: 47 additions & 0 deletions packages/core-modules/contracts/modules/NftModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@synthetixio/core-contracts/contracts/token/ERC721.sol";
import "@synthetixio/core-contracts/contracts/utils/AddressUtil.sol";
import "@synthetixio/core-contracts/contracts/initializable/InitializableMixin.sol";
import "@synthetixio/core-contracts/contracts/ownership/OwnableMixin.sol";
import "@synthetixio/core-contracts/contracts/errors/AddressError.sol";

import "../storage/NftStorage.sol";

import "../interfaces/INftModule.sol";

contract NftModule is INftModule, ERC721, NftStorage, InitializableMixin, OwnableMixin {
event Mint(address owner, uint nftId);

// ---------------------------------------
// Chores
// ---------------------------------------
function _isInitialized() internal view override returns (bool) {
return _nftStore().initialized;
}

function isInitialized() external view returns (bool) {
return _isInitialized();
}

function initialize(
string memory tokenName,
string memory tokenSymbol,
string memory uri
) public onlyOwner {
_initialize(tokenName, tokenSymbol, uri);
NftStore storage store = _nftStore();

store.initialized = true;
}

// ---------------------------------------
// Mint/Transfer
// ---------------------------------------
function mint(address owner, uint256 nftId) external override onlyOwner {
_mint(owner, nftId);

emit Mint(owner, nftId);
}
}

0 comments on commit 2bb404a

Please sign in to comment.