diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..c8bc4b4ba --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,53 @@ +name: Lint and Test +on: + push: + pull_request: + types: [opened, edited, synchronize, reopened] +jobs: + build: + name: Build + runs-on: ubuntu-latest + container: python:3.10.5 + steps: + - name: checkout + uses: actions/checkout@v3 + - name: Use Node.js 16.x + uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Install yarn + run: npm install -g yarn + - name: Pull submodules + run: git submodule init && git submodule update + - name: Install dependencies + run: | + yarn install --frozen-lockfile + pip3 install -U pip setuptools virtualenv + pip3 install -r requirements-dev.txt + - name: Build + run: | + yarn build + yarn typecheck + yarn workspace @trusttoken-smart-contracts/contracts-por build + yarn workspace @trusttoken-smart-contracts/contracts-tron build + - name: lint + if: success() || failure() + run: yarn lint + - name: lint por + if: success() || failure() + run: yarn workspace @trusttoken-smart-contracts/contracts-por lint + - name: test por + if: success() || failure() + run: yarn workspace @trusttoken-smart-contracts/contracts-por test + # - name: lint tron + # if: success() || failure() + # run: yarn workspace @trusttoken-smart-contracts/contracts-tron lint + - name: test tron + if: success() || failure() + run: yarn workspace @trusttoken-smart-contracts/contracts-tron test + - name: test-others + if: success() || failure() + run: | + yarn test:proxy + yarn test:registry + yarn test:true-currencies diff --git a/packages/contracts-tron/.compiler.json b/packages/contracts-tron/.compiler.json new file mode 100644 index 000000000..ec1194831 --- /dev/null +++ b/packages/contracts-tron/.compiler.json @@ -0,0 +1,9 @@ +{ + "version": "0.6.0", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + } + } +} diff --git a/packages/contracts-tron/.eslintrc.json b/packages/contracts-tron/.eslintrc.json new file mode 100644 index 000000000..aeec3fda6 --- /dev/null +++ b/packages/contracts-tron/.eslintrc.json @@ -0,0 +1,439 @@ +{ + "env": { + "es6": true + }, + "extends": [ + "eslint:recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "plugins": [ + "no-only-tests" + ], + "rules": { + "accessor-pairs": "error", + "array-bracket-spacing": [ + "error", + "never" + ], + "arrow-spacing": [ + "error", + { + "after": true, + "before": true + } + ], + "block-spacing": [ + "error", + "always" + ], + "brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": true + } + ], + "camelcase": "off", + "comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "exports": "always-multiline", + "functions": "always-multiline", + "imports": "always-multiline", + "objects": "always-multiline" + } + ], + "comma-spacing": [ + "error", + { + "after": true, + "before": false + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "constructor-super": "error", + "curly": [ + "error", + "multi-line" + ], + "dot-location": [ + "error", + "property" + ], + "eol-last": "error", + "eqeqeq": [ + "error", + "always", + { + "null": "ignore" + } + ], + "func-call-spacing": [ + "error", + "never" + ], + "generator-star-spacing": [ + "error", + { + "after": true, + "before": true + } + ], + "handle-callback-err": [ + "error", + "^(err|error)$" + ], + "indent": ["error", 2], + "key-spacing": [ + "error", + { + "afterColon": true, + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "after": true, + "before": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-between-class-members": [ + "error", + "always", + { + "exceptAfterSingleLine": true + } + ], + "max-len": "off", + "new-cap": [ + "error", + { + "capIsNew": false, + "newIsCap": true + } + ], + "new-parens": "error", + "no-only-tests/no-only-tests": ["error", {"fix": true}], + "no-array-constructor": "error", + "no-async-promise-executor": "error", + "no-caller": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-condition": [ + "error", + { + "checkLoops": false + } + ], + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-eval": "error", + "no-ex-assign": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-boolean-cast": "error", + "no-extra-parens": [ + "error", + "functions" + ], + "no-fallthrough": "error", + "no-floating-decimal": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-implied-eval": "error", + "no-inner-declarations": [ + "error", + "functions" + ], + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": [ + "error", + { + "allowLoop": false, + "allowSwitch": false + } + ], + "no-lone-blocks": "error", + "no-misleading-character-class": "error", + "no-mixed-operators": [ + "error", + { + "allowSamePrecedence": true, + "groups": [ + [ + "==", + "!=", + "===", + "!==", + ">", + ">=", + "<", + "<=" + ], + [ + "&&", + "||" + ], + [ + "in", + "instanceof" + ] + ] + } + ], + "no-mixed-spaces-and-tabs": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 1, + "maxEOF": 0 + } + ], + "no-negated-in-lhs": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-object": "error", + "no-new-require": "error", + "no-new-symbol": "error", + "no-new-wrappers": "error", + "no-obj-calls": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-proto": "error", + "no-prototype-builtins": "error", + "no-redeclare": [ + "error", + { + "builtinGlobals": false + } + ], + "no-regex-spaces": "error", + "no-return-assign": [ + "error", + "except-parens" + ], + "no-return-await": "error", + "no-self-assign": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-tabs": "error", + "no-template-curly-in-string": "error", + "no-this-before-super": "error", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef": "off", + "no-unexpected-multiline": "error", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": [ + "error", + { + "defaultAssignment": false + } + ], + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], + "no-restricted-imports": ["error", { + "patterns": ["original-works-core/src", "original-works-core/dist"] + }], + "no-use-before-define": [ + "error", + { + "classes": false, + "functions": false, + "variables": false + } + ], + "no-useless-call": "error", + "no-useless-catch": "error", + "no-useless-computed-key": "error", + "no-useless-escape": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-whitespace-before-property": "error", + "no-with": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "object-property-newline": [ + "error", + { + "allowMultiplePropertiesPerLine": true + } + ], + "one-var": [ + "error", + { + "initialized": "never" + } + ], + "operator-linebreak": [ + "error", + "after", + { + "overrides": { + ":": "before", + "?": "before" + } + } + ], + "padded-blocks": [ + "error", + { + "blocks": "never", + "classes": "never", + "switches": "never" + } + ], + "prefer-const": [ + "error", + { + "destructuring": "all" + } + ], + "prefer-promise-reject-errors": "error", + "quote-props": [ + "error", + "as-needed" + ], + "quotes": [ + "error", + "single", + { + "avoidEscape": true + } + ], + "rest-spread-spacing": [ + "error", + "never" + ], + "semi": [ + "error", + "never" + ], + "semi-spacing": [ + "error", + { + "after": true, + "before": false + } + ], + "space-before-blocks": [ + "error", + "always" + ], + "space-before-function-paren": [ + "error", + "always" + ], + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-unary-ops": [ + "error", + { + "nonwords": false, + "words": true + } + ], + "spaced-comment": [ + "error", + "always", + { + "block": { + "balanced": true, + "exceptions": [ + "*" + ], + "markers": [ + "*package", + "!", + ",", + ":", + "::", + "flow-include" + ] + }, + "line": { + "markers": [ + "*package", + "!", + "/", + ",", + "=" + ] + } + } + ], + "symbol-description": "error", + "template-curly-spacing": [ + "error", + "never" + ], + "template-tag-spacing": [ + "error", + "never" + ], + "unicode-bom": [ + "error", + "never" + ], + "use-isnan": "error", + "valid-typeof": [ + "error", + { + "requireStringLiterals": true + } + ], + "wrap-iife": [ + "error", + "any", + { + "functionPrototypeMethods": true + } + ], + "yield-star-spacing": [ + "error", + "both" + ], + "yoda": [ + "error", + "never" + ] + } +} diff --git a/packages/contracts-tron/.eslintrc.typescript.js b/packages/contracts-tron/.eslintrc.typescript.js new file mode 100644 index 000000000..6ecdb1aff --- /dev/null +++ b/packages/contracts-tron/.eslintrc.typescript.js @@ -0,0 +1,86 @@ +const baseConfig = require('./.eslintrc.json') + +module.exports = { + ...baseConfig, + parser: '@typescript-eslint/parser', + extends: [ + "plugin:@typescript-eslint/recommended" + ], + rules: { + ...baseConfig.rules, + "@typescript-eslint/camelcase": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/explicit-member-accessibility": [ + "error", + { + "accessibility": "no-public", + "overrides": { + "parameterProperties": "off" + } + } + ], + "@typescript-eslint/indent": [ + "error", + 2, + { + "ArrayExpression": 1, + "CallExpression": { + "arguments": 1 + }, + "FunctionDeclaration": { + "body": 1, + "parameters": 1 + }, + "FunctionExpression": { + "body": 1, + "parameters": 1 + }, + "ImportDeclaration": 1, + "MemberExpression": 1, + "ObjectExpression": 1, + "SwitchCase": 1, + "VariableDeclarator": 1, + "flatTernaryExpressions": false, + "ignoreComments": false, + "outerIIFEBody": 1 + } + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": [ + "error", + { + "multiline": { + "delimiter": "comma", + "requireLast": true + }, + "singleline": { + "delimiter": "comma", + "requireLast": false + } + } + ], + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "args": "none", + "ignoreRestSiblings": true, + "vars": "all" + } + ], + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/ban-ts-comment": "off", + "space-before-function-paren": [ + "error", + { + "anonymous": "always", + "named": "never" + } + ], + }, +} diff --git a/packages/contracts-tron/.gitignore b/packages/contracts-tron/.gitignore new file mode 100644 index 000000000..89a888b43 --- /dev/null +++ b/packages/contracts-tron/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/build +/cache +/flattened_contracts diff --git a/packages/contracts-tron/.mocharc.json b/packages/contracts-tron/.mocharc.json new file mode 100644 index 000000000..e0519da05 --- /dev/null +++ b/packages/contracts-tron/.mocharc.json @@ -0,0 +1,8 @@ +{ + "require": ["ts-node/register/transpile-only", "tsconfig-paths/register"], + "extension": ["ts"], + "target": "esnext", + "timeout": 40000, + "watch-files": ["test"], + "exit": true +} diff --git a/packages/contracts-tron/.solhint.json b/packages/contracts-tron/.solhint.json new file mode 100644 index 000000000..4efafc53d --- /dev/null +++ b/packages/contracts-tron/.solhint.json @@ -0,0 +1,17 @@ +{ + "extends": "solhint:recommended", + "rules": { + "avoid-suicide": "error", + "compiler-version": ["error","0.6.0"], + "state-visibility": "off", + "func-name-mixedcase": "off", + "var-name-mixedcase": "off", + "no-empty-blocks": "off", + "no-inline-assembly": "off", + "not-rely-on-time": "off", + "max-states-count": "off", + "reason-string": "off", + "const-name-snakecase": "off", + "avoid-tx-origin": "off" + } +} diff --git a/packages/contracts-tron/.waffle.json b/packages/contracts-tron/.waffle.json new file mode 100644 index 000000000..8c7690b7b --- /dev/null +++ b/packages/contracts-tron/.waffle.json @@ -0,0 +1,11 @@ +{ + "compilerVersion": "v0.6.0+commit.26b70077", + "compilerOptions": { + "optimizer": { + "enabled": true, + "runs": 20000 + } + }, + "sourceDirectory": "./contracts", + "flattenOutputDirectory": "./flattened_contracts" + } diff --git a/packages/contracts-tron/abi-exporter.js b/packages/contracts-tron/abi-exporter.js new file mode 100644 index 000000000..5e1609916 --- /dev/null +++ b/packages/contracts-tron/abi-exporter.js @@ -0,0 +1,70 @@ +const fs = require('fs') +const path = require('path') +const { extendConfig } = require('hardhat/config') + +const { HardhatPluginError } = require('hardhat/plugins') + +const { + TASK_COMPILE, +} = require('hardhat/builtin-tasks/task-names') + +extendConfig(function (config, userConfig) { + config.abiExporter = Object.assign( + { + path: './abi', + clear: false, + flat: false, + only: [], + except: [], + spacing: 2, + }, + userConfig.abiExporter, + ) +}) + +task(TASK_COMPILE, async function (args, hre, runSuper) { + const config = hre.config.abiExporter + + await runSuper() + + const outputDirectory = path.resolve(hre.config.paths.root, config.path) + + if (!outputDirectory.startsWith(hre.config.paths.root)) { + throw new HardhatPluginError('resolved path must be inside of project directory') + } + + if (outputDirectory === hre.config.paths.root) { + throw new HardhatPluginError('resolved path must not be root directory') + } + + if (config.clear) { + if (fs.existsSync(outputDirectory)) { + fs.rmdirSync(outputDirectory, { recursive: true }) + } + } + + if (!fs.existsSync(outputDirectory)) { + fs.mkdirSync(outputDirectory, { recursive: true }) + } + + for (const fullName of await hre.artifacts.getAllFullyQualifiedNames()) { + if (config.only.length && !config.only.some(m => fullName.match(m))) continue + if (config.except.length && config.except.some(m => fullName.match(m))) continue + + const { abi, sourceName, contractName, bytecode, deployedBytecode } = await hre.artifacts.readArtifact(fullName) + + if (!abi.length) continue + + const destination = path.resolve( + outputDirectory, + config.flat ? '' : sourceName, + contractName, + ) + '.json' + + if (!fs.existsSync(path.dirname(destination))) { + fs.mkdirSync(path.dirname(destination), { recursive: true }) + } + + fs.writeFileSync(destination, `${JSON.stringify({ abi, bytecode, deployedBytecode }, null, config.spacing)}\n`, { flag: 'w' }) + } +}) diff --git a/packages/contracts-tron/contracts/OwnedUpgradeabilityProxy.sol b/packages/contracts-tron/contracts/OwnedUpgradeabilityProxy.sol new file mode 100644 index 000000000..82a49fd3b --- /dev/null +++ b/packages/contracts-tron/contracts/OwnedUpgradeabilityProxy.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +// solhint-disable const-name-snakecase +pragma solidity ^0.6.0; + +/** + * @title OwnedUpgradeabilityProxy + * @dev This contract combines an upgradeability proxy with basic authorization control functionalities + */ +contract OwnedUpgradeabilityProxy { + /** + * @dev Event to show ownership has been transferred + * @param previousOwner representing the address of the previous owner + * @param newOwner representing the address of the new owner + */ + event ProxyOwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Event to show ownership transfer is pending + * @param currentOwner representing the address of the current owner + * @param pendingOwner representing the address of the pending owner + */ + event NewPendingOwner(address currentOwner, address pendingOwner); + + // Storage position of the owner and pendingOwner of the contract + bytes32 private constant proxyOwnerPosition = 0x6279e8199720cf3557ecd8b58d667c8edc486bd1cf3ad59ea9ebdfcae0d0dfac; //keccak256("trueUSD.proxy.owner"); + bytes32 private constant pendingProxyOwnerPosition = 0x8ddbac328deee8d986ec3a7b933a196f96986cb4ee030d86cc56431c728b83f4; //keccak256("trueUSD.pending.proxy.owner"); + + /** + * @dev the constructor sets the original owner of the contract to the sender account. + */ + constructor() public { + _setUpgradeabilityOwner(msg.sender); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyProxyOwner() { + require(msg.sender == proxyOwner(), "only Proxy Owner"); + _; + } + + /** + * @dev Throws if called by any account other than the pending owner. + */ + modifier onlyPendingProxyOwner() { + require(msg.sender == pendingProxyOwner(), "only pending Proxy Owner"); + _; + } + + /** + * @dev Tells the address of the owner + * @return owner the address of the owner + */ + function proxyOwner() public view returns (address owner) { + bytes32 position = proxyOwnerPosition; + assembly { + owner := sload(position) + } + } + + /** + * @dev Tells the address of the owner + * @return pendingOwner the address of the pending owner + */ + function pendingProxyOwner() public view returns (address pendingOwner) { + bytes32 position = pendingProxyOwnerPosition; + assembly { + pendingOwner := sload(position) + } + } + + /** + * @dev Sets the address of the owner + */ + function _setUpgradeabilityOwner(address newProxyOwner) internal { + bytes32 position = proxyOwnerPosition; + assembly { + sstore(position, newProxyOwner) + } + } + + /** + * @dev Sets the address of the owner + */ + function _setPendingUpgradeabilityOwner(address newPendingProxyOwner) internal { + bytes32 position = pendingProxyOwnerPosition; + assembly { + sstore(position, newPendingProxyOwner) + } + } + + /** + * @dev Allows the current owner to transfer control of the contract to a newOwner. + *changes the pending owner to newOwner. But doesn't actually transfer + * @param newOwner The address to transfer ownership to. + */ + function transferProxyOwnership(address newOwner) external onlyProxyOwner { + require(newOwner != address(0)); + _setPendingUpgradeabilityOwner(newOwner); + emit NewPendingOwner(proxyOwner(), newOwner); + } + + /** + * @dev Allows the pendingOwner to claim ownership of the proxy + */ + function claimProxyOwnership() external onlyPendingProxyOwner { + emit ProxyOwnershipTransferred(proxyOwner(), pendingProxyOwner()); + _setUpgradeabilityOwner(pendingProxyOwner()); + _setPendingUpgradeabilityOwner(address(0)); + } + + /** + * @dev Allows the proxy owner to upgrade the current version of the proxy. + * @param implementation representing the address of the new implementation to be set. + */ + function upgradeTo(address implementation) public virtual onlyProxyOwner { + address currentImplementation; + bytes32 position = implementationPosition; + assembly { + currentImplementation := sload(position) + } + require(currentImplementation != implementation); + assembly { + sstore(position, implementation) + } + emit Upgraded(implementation); + } + + /** + * @dev This event will be emitted every time the implementation gets upgraded + * @param implementation representing the address of the upgraded implementation + */ + event Upgraded(address indexed implementation); + + // Storage position of the address of the current implementation + bytes32 private constant implementationPosition = 0x6e41e0fbe643dfdb6043698bf865aada82dc46b953f754a3468eaa272a362dc7; //keccak256("trueUSD.proxy.implementation"); + + function implementation() public view returns (address impl) { + bytes32 position = implementationPosition; + assembly { + impl := sload(position) + } + } + + /** + * @dev Fallback functions allowing to perform a delegatecall to the given implementation. + * This function will return whatever the implementation call returns + */ + fallback() external payable { + proxyCall(); + } + + receive() external payable { + proxyCall(); + } + + function proxyCall() internal { + bytes32 position = implementationPosition; + + assembly { + let ptr := mload(0x40) + calldatacopy(ptr, returndatasize(), calldatasize()) + let result := delegatecall(gas(), sload(position), ptr, calldatasize(), returndatasize(), returndatasize()) + returndatacopy(ptr, 0, returndatasize()) + + switch result + case 0 { + revert(ptr, returndatasize()) + } + default { + return(ptr, returndatasize()) + } + } + } +} diff --git a/packages/contracts-tron/contracts/Registry.sol b/packages/contracts-tron/contracts/Registry.sol new file mode 100644 index 000000000..f9e8c033d --- /dev/null +++ b/packages/contracts-tron/contracts/Registry.sol @@ -0,0 +1,206 @@ +pragma solidity ^0.6.0; + +import {ITRC20} from "./interface/ITRC20.sol"; + +interface RegistryClone { + function syncAttributeValue( + address _who, + bytes32 _attribute, + uint256 _value + ) external; +} + +contract Registry { + struct AttributeData { + uint256 value; + bytes32 notes; + address adminAddr; + uint256 timestamp; + } + + // never remove any storage variables + address public owner; + address public pendingOwner; + bool initialized; + + // Stores arbitrary attributes for users. An example use case is an ITRC20 + // token that requires its users to go through a KYC/AML check - in this case + // a validator can set an account's "hasPassedKYC/AML" attribute to 1 to indicate + // that account can use the token. This mapping stores that value (1, in the + // example) as well as which validator last set the value and at what time, + // so that e.g. the check can be renewed at appropriate intervals. + mapping(address => mapping(bytes32 => AttributeData)) attributes; + // The logic governing who is allowed to set what attributes is abstracted as + // this accessManager, so that it may be replaced by the owner as needed + bytes32 constant WRITE_PERMISSION = keccak256("canWriteTo-"); + mapping(bytes32 => RegistryClone[]) subscribers; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event SetAttribute(address indexed who, bytes32 attribute, uint256 value, bytes32 notes, address indexed adminAddr); + event SetManager(address indexed oldManager, address indexed newManager); + event StartSubscription(bytes32 indexed attribute, RegistryClone indexed subscriber); + event StopSubscription(bytes32 indexed attribute, RegistryClone indexed subscriber); + + /** + * @dev sets the original `owner` of the contract to the sender + * at construction. Must then be reinitialized + */ + constructor() public { + owner = msg.sender; + emit OwnershipTransferred(address(0), owner); + } + + function initialize() public { + require(!initialized, "already initialized"); + owner = msg.sender; + emit OwnershipTransferred(address(0), owner); + initialized = true; + } + + // Allows a write if either a) the writer is that Registry's owner, or + // b) the writer is writing to attribute foo and that writer already has + // the canWriteTo-foo attribute set (in that same Registry) + function confirmWrite(bytes32 _attribute, address _admin) internal view returns (bool) { + return (_admin == owner || hasAttribute(_admin, keccak256(abi.encodePacked(WRITE_PERMISSION ^ _attribute)))); + } + + // Writes are allowed only if the accessManager approves + function setAttribute( + address _who, + bytes32 _attribute, + uint256 _value, + bytes32 _notes + ) public { + require(confirmWrite(_attribute, msg.sender)); + attributes[_who][_attribute] = AttributeData(_value, _notes, msg.sender, block.timestamp); + emit SetAttribute(_who, _attribute, _value, _notes, msg.sender); + + RegistryClone[] storage targets = subscribers[_attribute]; + uint256 index = targets.length; + while (index-- > 0) { + targets[index].syncAttributeValue(_who, _attribute, _value); + } + } + + function subscribe(bytes32 _attribute, RegistryClone _syncer) external onlyOwner { + subscribers[_attribute].push(_syncer); + emit StartSubscription(_attribute, _syncer); + } + + function unsubscribe(bytes32 _attribute, uint256 _index) external onlyOwner { + uint256 length = subscribers[_attribute].length; + require(_index < length); + emit StopSubscription(_attribute, subscribers[_attribute][_index]); + subscribers[_attribute][_index] = subscribers[_attribute][length - 1]; + subscribers[_attribute].pop(); + } + + function subscriberCount(bytes32 _attribute) public view returns (uint256) { + return subscribers[_attribute].length; + } + + function setAttributeValue( + address _who, + bytes32 _attribute, + uint256 _value + ) public { + require(confirmWrite(_attribute, msg.sender)); + attributes[_who][_attribute] = AttributeData(_value, "", msg.sender, block.timestamp); + emit SetAttribute(_who, _attribute, _value, "", msg.sender); + RegistryClone[] storage targets = subscribers[_attribute]; + uint256 index = targets.length; + while (index-- > 0) { + targets[index].syncAttributeValue(_who, _attribute, _value); + } + } + + // Returns true if the uint256 value stored for this attribute is non-zero + function hasAttribute(address _who, bytes32 _attribute) public view returns (bool) { + return attributes[_who][_attribute].value != 0; + } + + // Returns the exact value of the attribute, as well as its metadata + function getAttribute(address _who, bytes32 _attribute) + public + view + returns ( + uint256, + bytes32, + address, + uint256 + ) + { + AttributeData memory data = attributes[_who][_attribute]; + return (data.value, data.notes, data.adminAddr, data.timestamp); + } + + function getAttributeValue(address _who, bytes32 _attribute) public view returns (uint256) { + return attributes[_who][_attribute].value; + } + + function getAttributeAdminAddr(address _who, bytes32 _attribute) public view returns (address) { + return attributes[_who][_attribute].adminAddr; + } + + function getAttributeTimestamp(address _who, bytes32 _attribute) public view returns (uint256) { + return attributes[_who][_attribute].timestamp; + } + + function syncAttribute( + bytes32 _attribute, + uint256 _startIndex, + address[] calldata _addresses + ) external { + RegistryClone[] storage targets = subscribers[_attribute]; + uint256 index = targets.length; + while (index-- > _startIndex) { + RegistryClone target = targets[index]; + for (uint256 i = _addresses.length; i-- > 0; ) { + address who = _addresses[i]; + target.syncAttributeValue(who, _attribute, attributes[who][_attribute].value); + } + } + } + + function reclaimTrx(address payable _to) external onlyOwner { + _to.transfer(address(this).balance); + } + + function reclaimToken(ITRC20 token, address _to) external onlyOwner { + uint256 balance = token.balanceOf(address(this)); + token.transfer(_to, balance); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner, "only Owner"); + _; + } + + /** + * @dev Modifier throws if called by any account other than the pendingOwner. + */ + modifier onlyPendingOwner() { + require(msg.sender == pendingOwner); + _; + } + + /** + * @dev Allows the current owner to set the pendingOwner address. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + pendingOwner = newOwner; + } + + /** + * @dev Allows the pendingOwner address to finalize the transfer. + */ + function claimOwnership() public onlyPendingOwner { + emit OwnershipTransferred(owner, pendingOwner); + owner = pendingOwner; + pendingOwner = address(0); + } +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/TronTokenController.sol b/packages/contracts-tron/contracts/TronTokenController.sol new file mode 100644 index 000000000..b91a505d6 --- /dev/null +++ b/packages/contracts-tron/contracts/TronTokenController.sol @@ -0,0 +1,648 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; +import {ITRC20} from "./interface/ITRC20.sol"; +import {Registry} from "./Registry.sol"; +import {OwnedUpgradeabilityProxy} from "./OwnedUpgradeabilityProxy.sol"; +import {TrueCurrency} from "./common/TrueCurrency.sol"; + +interface IHasOwner { + function claimOwnership() external; + + function transferOwnership(address newOwner) external; +} + +/** @title TokenController + * @dev This contract allows us to split ownership of the TrueCurrency contract + * into two addresses. One, called the "owner" address, has unfettered control of the TrueCurrency contract - + * it can mint new tokens, transfer ownership of the contract, etc. However to make + * extra sure that TrueCurrency is never compromised, this owner key will not be used in + * day-to-day operations, allowing it to be stored at a heightened level of security. + * Instead, the owner appoints an various "admin" address. + * There are 3 different types of admin addresses; MintKey, MintRatifier, and MintPauser. + * MintKey can request and revoke mints one at a time. + * MintPausers can pause individual mints or pause all mints. + * MintRatifiers can approve and finalize mints with enough approval. + + * There are three levels of mints: instant mint, ratified mint, and multiSig mint. Each have a different threshold + * and deduct from a different pool. + * Instant mint has the lowest threshold and finalizes instantly without any ratifiers. Deduct from instant mint pool, + * which can be refilled by one ratifier. + * Ratify mint has the second lowest threshold and finalizes with one ratifier approval. Deduct from ratify mint pool, + * which can be refilled by three ratifiers. + * MultiSig mint has the highest threshold and finalizes with three ratifier approvals. Deduct from multiSig mint pool, + * which can only be refilled by the owner. +*/ + +contract TokenController { + using SafeMath for uint256; + + struct MintOperation { + address to; + uint256 value; + uint256 requestedBlock; + uint256 numberOfApproval; + bool paused; + mapping(address => bool) approved; + } + + address payable public owner; + address payable public pendingOwner; + + bool public initialized; + + uint256 public instantMintThreshold; + uint256 public ratifiedMintThreshold; + uint256 public multiSigMintThreshold; + + uint256 public instantMintLimit; + uint256 public ratifiedMintLimit; + uint256 public multiSigMintLimit; + + uint256 public instantMintPool; + uint256 public ratifiedMintPool; + uint256 public multiSigMintPool; + address[2] public ratifiedPoolRefillApprovals; + + uint8 public constant RATIFY_MINT_SIGS = 1; //number of approvals needed to finalize a Ratified Mint + uint8 public constant MULTISIG_MINT_SIGS = 3; //number of approvals needed to finalize a MultiSig Mint + + bool public mintPaused; + uint256 public mintReqInvalidBeforeThisBlock; //all mint request before this block are invalid + address public mintKey; + MintOperation[] public mintOperations; //list of a mint requests + + TrueCurrency public token; + Registry public registry; + address public registryAdmin; + + // Registry attributes for admin keys + bytes32 public constant IS_MINT_PAUSER = "isTUSDMintPausers"; + bytes32 public constant IS_MINT_RATIFIER = "isTUSDMintRatifier"; + bytes32 public constant IS_REDEMPTION_ADMIN = "isTUSDRedemptionAdmin"; + + // paused version of TrueCurrency in Production + // pausing the contract upgrades the proxy to this implementation + address public constant PAUSED_IMPLEMENTATION = 0x9821722854922f004a9EE24d8aedF3D422745062; + + modifier onlyMintKeyOrOwner() { + require(msg.sender == mintKey || msg.sender == owner, "must be mintKey or owner"); + _; + } + + modifier onlyMintPauserOrOwner() { + require(registry.hasAttribute(msg.sender, IS_MINT_PAUSER) || msg.sender == owner, "must be pauser or owner"); + _; + } + + modifier onlyMintRatifierOrOwner() { + require(registry.hasAttribute(msg.sender, IS_MINT_RATIFIER) || msg.sender == owner, "must be ratifier or owner"); + _; + } + + modifier onlyOwnerOrRedemptionAdmin() { + require(registry.hasAttribute(msg.sender, IS_REDEMPTION_ADMIN) || msg.sender == owner, "must be Redemption admin or owner"); + _; + } + + + modifier onlyRegistryAdmin() { + require(msg.sender == registryAdmin || msg.sender == owner, "must be registry admin or owner"); + _; + } + + //mint operations by the mintkey cannot be processed on when mints are paused + modifier mintNotPaused() { + if (msg.sender != owner) { + require(!mintPaused, "minting is paused"); + } + _; + } + /// @dev Emitted when ownership of controller was transferred + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + /// @dev Emitted when ownership of controller transfer procedure was started + event NewOwnerPending(address indexed currentOwner, address indexed pendingOwner); + /// @dev Emitted when new registry was set + event SetRegistry(address indexed registry); + /// @dev Emitted when owner was transferred for child contract + event TransferChild(address indexed child, address indexed newOwner); + /// @dev Emitted when child ownership was claimed + event RequestReclaimContract(address indexed other); + /// @dev Emitted when child token was changed + event SetToken(TrueCurrency newContract); + /// @dev Emitted when canBurn status of the `burner` was changed to `canBurn` + event CanBurn(address burner, bool canBurn); + + /// @dev Emitted when mint was requested + event RequestMint(address indexed to, uint256 indexed value, uint256 opIndex, address mintKey); + /// @dev Emitted when mint was finalized + event FinalizeMint(address indexed to, uint256 indexed value, uint256 opIndex, address mintKey); + /// @dev Emitted on instant mint + event InstantMint(address indexed to, uint256 indexed value, address indexed mintKey); + + /// @dev Emitted when mint key was replaced + event TransferMintKey(address indexed previousMintKey, address indexed newMintKey); + /// @dev Emitted when mint was ratified + event MintRatified(uint256 indexed opIndex, address indexed ratifier); + /// @dev Emitted when mint is revoked + event RevokeMint(uint256 opIndex); + /// @dev Emitted when all mining is paused (status=true) or unpaused (status=false) + event AllMintsPaused(bool status); + /// @dev Emitted when opIndex mint is paused (status=true) or unpaused (status=false) + event MintPaused(uint256 opIndex, bool status); + /// @dev Emitted when mint is approved + event MintApproved(address approver, uint256 opIndex); + /// @dev Emitted when fast pause contract is changed + event FastPauseSet(address _newFastPause); + + /// @dev Emitted when mint threshold changes + event MintThresholdChanged(uint256 instant, uint256 ratified, uint256 multiSig); + /// @dev Emitted when mint limits change + event MintLimitsChanged(uint256 instant, uint256 ratified, uint256 multiSig); + /// @dev Emitted when instant mint pool is refilled + event InstantPoolRefilled(); + /// @dev Emitted when instant mint pool is ratified + event RatifyPoolRefilled(); + /// @dev Emitted when multisig mint pool is ratified + event MultiSigPoolRefilled(); + + /* + ======================================== + Ownership functions + ======================================== + */ + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner, "only Owner"); + _; + } + + /** + * @dev Modifier throws if called by any account other than the pendingOwner. + */ + modifier onlyPendingOwner() { + require(msg.sender == pendingOwner); + _; + } + + /** + * @dev sets the original `owner` of the contract to the sender + * at construction. Must then be reinitialized + */ + constructor() public { + owner = msg.sender; + emit OwnershipTransferred(address(0), owner); + } + + function initialize() public { + require(!initialized, "already initialized"); + owner = msg.sender; + emit OwnershipTransferred(address(0), owner); + + instantMintThreshold = 150_000_000_000_000_000_000_000_000; + ratifiedMintThreshold = 300_000_000_000_000_000_000_000_000; + multiSigMintThreshold = 1_000_000_000_000_000_000_000_000_000; + emit MintThresholdChanged(150_000_000_000_000_000_000_000_000, 300_000_000_000_000_000_000_000_000, 1_000_000_000_000_000_000_000_000_000); + instantMintLimit = 150_000_000_000_000_000_000_000_000; + ratifiedMintLimit = 300_000_000_000_000_000_000_000_000; + multiSigMintLimit = 1_000_000_000_000_000_000_000_000_000; + emit MintLimitsChanged(150_000_000_000_000_000_000_000_000, 300_000_000_000_000_000_000_000_000, 1_000_000_000_000_000_000_000_000_000); + + initialized = true; + } + + /** + * @dev Allows the current owner to set the pendingOwner address. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address payable newOwner) external onlyOwner { + pendingOwner = newOwner; + emit NewOwnerPending(address(owner), address(pendingOwner)); + } + + /** + * @dev Allows the pendingOwner address to finalize the transfer. + */ + function claimOwnership() external onlyPendingOwner { + emit OwnershipTransferred(address(owner), address(pendingOwner)); + owner = pendingOwner; + pendingOwner = address(0); + } + + /* + ======================================== + proxy functions + ======================================== + */ + + function transferTrueCurrencyProxyOwnership(address _newOwner) external onlyOwner { + OwnedUpgradeabilityProxy(address(uint160(address(token)))).transferProxyOwnership(_newOwner); + } + + function claimTrueCurrencyProxyOwnership() external onlyOwner { + OwnedUpgradeabilityProxy(address(uint160(address(token)))).claimProxyOwnership(); + } + + function upgradeTrueCurrencyProxyImplTo(address _implementation) external onlyOwner { + OwnedUpgradeabilityProxy(address(uint160(address(token)))).upgradeTo(_implementation); + } + + /* + ======================================== + Minting functions + ======================================== + */ + + /** + * @dev set the threshold for a mint to be considered an instant mint, + * ratify mint and multiSig mint. Instant mint requires no approval, + * ratify mint requires 1 approval and multiSig mint requires 3 approvals + */ + function setMintThresholds( + uint256 _instant, + uint256 _ratified, + uint256 _multiSig + ) external onlyOwner { + require(_instant <= _ratified && _ratified <= _multiSig); + instantMintThreshold = _instant; + ratifiedMintThreshold = _ratified; + multiSigMintThreshold = _multiSig; + emit MintThresholdChanged(_instant, _ratified, _multiSig); + } + + /** + * @dev set the limit of each mint pool. For example can only instant mint up to the instant mint pool limit + * before needing to refill + */ + function setMintLimits( + uint256 _instant, + uint256 _ratified, + uint256 _multiSig + ) external onlyOwner { + require(_instant <= _ratified && _ratified <= _multiSig); + instantMintLimit = _instant; + if (instantMintPool > instantMintLimit) { + instantMintPool = instantMintLimit; + } + ratifiedMintLimit = _ratified; + if (ratifiedMintPool > ratifiedMintLimit) { + ratifiedMintPool = ratifiedMintLimit; + } + multiSigMintLimit = _multiSig; + if (multiSigMintPool > multiSigMintLimit) { + multiSigMintPool = multiSigMintLimit; + } + emit MintLimitsChanged(_instant, _ratified, _multiSig); + } + + /** + * @dev Ratifier can refill instant mint pool + */ + function refillInstantMintPool() external onlyMintRatifierOrOwner { + ratifiedMintPool = ratifiedMintPool.sub(instantMintLimit.sub(instantMintPool)); + instantMintPool = instantMintLimit; + emit InstantPoolRefilled(); + } + + /** + * @dev Owner or 3 ratifiers can refill Ratified Mint Pool + */ + function refillRatifiedMintPool() external onlyMintRatifierOrOwner { + if (msg.sender != owner) { + address[2] memory refillApprovals = ratifiedPoolRefillApprovals; + require(msg.sender != refillApprovals[0] && msg.sender != refillApprovals[1]); + if (refillApprovals[0] == address(0)) { + ratifiedPoolRefillApprovals[0] = msg.sender; + return; + } + if (refillApprovals[1] == address(0)) { + ratifiedPoolRefillApprovals[1] = msg.sender; + return; + } + } + delete ratifiedPoolRefillApprovals; // clears the whole array + multiSigMintPool = multiSigMintPool.sub(ratifiedMintLimit.sub(ratifiedMintPool)); + ratifiedMintPool = ratifiedMintLimit; + emit RatifyPoolRefilled(); + } + + /** + * @dev Owner can refill MultiSig Mint Pool + */ + function refillMultiSigMintPool() external onlyOwner { + multiSigMintPool = multiSigMintLimit; + emit MultiSigPoolRefilled(); + } + + /** + * @dev mintKey initiates a request to mint _value for account _to + * @param _to the address to mint to + * @param _value the amount requested + */ + function requestMint(address _to, uint256 _value) external mintNotPaused onlyMintKeyOrOwner { + MintOperation memory op = MintOperation(_to, _value, block.number, 0, false); + emit RequestMint(_to, _value, mintOperations.length, msg.sender); + mintOperations.push(op); + } + + /** + * @dev Instant mint without ratification if the amount is less + * than instantMintThreshold and instantMintPool + * @param _to the address to mint to + * @param _value the amount minted + */ + function instantMint(address _to, uint256 _value) external mintNotPaused onlyMintKeyOrOwner { + require(_value <= instantMintThreshold, "over the instant mint threshold"); + require(_value <= instantMintPool, "instant mint pool is dry"); + instantMintPool = instantMintPool.sub(_value); + emit InstantMint(_to, _value, msg.sender); + token.mint(_to, _value); + } + + /** + * @dev ratifier ratifies a request mint. If the number of + * ratifiers that signed off is greater than the number of + * approvals required, the request is finalized + * @param _index the index of the requestMint to ratify + * @param _to the address to mint to + * @param _value the amount requested + */ + function ratifyMint( + uint256 _index, + address _to, + uint256 _value + ) external mintNotPaused onlyMintRatifierOrOwner { + MintOperation memory op = mintOperations[_index]; + require(op.to == _to, "to address does not match"); + require(op.value == _value, "amount does not match"); + require(!mintOperations[_index].approved[msg.sender], "already approved"); + mintOperations[_index].approved[msg.sender] = true; + mintOperations[_index].numberOfApproval = mintOperations[_index].numberOfApproval.add(1); + emit MintRatified(_index, msg.sender); + if (hasEnoughApproval(mintOperations[_index].numberOfApproval, _value)) { + finalizeMint(_index); + } + } + + /** + * @dev finalize a mint request, mint the amount requested to the specified address + * @param _index of the request (visible in the RequestMint event accompanying the original request) + */ + function finalizeMint(uint256 _index) public mintNotPaused { + MintOperation memory op = mintOperations[_index]; + address to = op.to; + uint256 value = op.value; + if (msg.sender != owner) { + require(canFinalize(_index)); + _subtractFromMintPool(value); + } + delete mintOperations[_index]; + token.mint(to, value); + emit FinalizeMint(to, value, _index, msg.sender); + } + + /** + * assumption: only invoked when canFinalize + */ + function _subtractFromMintPool(uint256 _value) internal { + if (_value <= ratifiedMintPool && _value <= ratifiedMintThreshold) { + ratifiedMintPool = ratifiedMintPool.sub(_value); + } else { + multiSigMintPool = multiSigMintPool.sub(_value); + } + } + + /** + * @dev compute if the number of approvals is enough for a given mint amount + */ + function hasEnoughApproval(uint256 _numberOfApproval, uint256 _value) public view returns (bool) { + if (_value <= ratifiedMintPool && _value <= ratifiedMintThreshold) { + if (_numberOfApproval >= RATIFY_MINT_SIGS) { + return true; + } + } + if (_value <= multiSigMintPool && _value <= multiSigMintThreshold) { + if (_numberOfApproval >= MULTISIG_MINT_SIGS) { + return true; + } + } + if (msg.sender == owner) { + return true; + } + return false; + } + + /** + * @dev compute if a mint request meets all the requirements to be finalized + * utility function for a front end + */ + function canFinalize(uint256 _index) public view returns (bool) { + MintOperation memory op = mintOperations[_index]; + require(op.requestedBlock > mintReqInvalidBeforeThisBlock, "this mint is invalid"); //also checks if request still exists + require(!op.paused, "this mint is paused"); + require(hasEnoughApproval(op.numberOfApproval, op.value), "not enough approvals"); + return true; + } + + /** + * @dev revoke a mint request, Delete the mintOperation + * @param _index of the request (visible in the RequestMint event accompanying the original request) + */ + function revokeMint(uint256 _index) external onlyMintKeyOrOwner { + delete mintOperations[_index]; + emit RevokeMint(_index); + } + + /** + * @dev get mint operatino count + * @return mint operation count + */ + function mintOperationCount() public view returns (uint256) { + return mintOperations.length; + } + + /* + ======================================== + Key management + ======================================== + */ + + /** + * @dev Replace the current mintkey with new mintkey + * @param _newMintKey address of the new mintKey + */ + function transferMintKey(address _newMintKey) external onlyOwner { + require(_newMintKey != address(0), "new mint key cannot be 0x0"); + emit TransferMintKey(mintKey, _newMintKey); + mintKey = _newMintKey; + } + + + function setRegistryAdmin(address admin) external onlyOwner { + registryAdmin = admin; + } + + /* + ======================================== + Mint Pausing + ======================================== + */ + + /** + * @dev invalidates all mint request initiated before the current block + */ + function invalidateAllPendingMints() external onlyOwner { + mintReqInvalidBeforeThisBlock = block.number; + } + + /** + * @dev pause any further mint request and mint finalizations + */ + function pauseMints() external onlyMintPauserOrOwner { + mintPaused = true; + emit AllMintsPaused(true); + } + + /** + * @dev unpause any further mint request and mint finalizations + */ + function unpauseMints() external onlyOwner { + mintPaused = false; + emit AllMintsPaused(false); + } + + /** + * @dev pause a specific mint request + * @param _opIndex the index of the mint request the caller wants to pause + */ + function pauseMint(uint256 _opIndex) external onlyMintPauserOrOwner { + mintOperations[_opIndex].paused = true; + emit MintPaused(_opIndex, true); + } + + /** + * @dev unpause a specific mint request + * @param _opIndex the index of the mint request the caller wants to unpause + */ + function unpauseMint(uint256 _opIndex) external onlyOwner { + mintOperations[_opIndex].paused = false; + emit MintPaused(_opIndex, false); + } + + /* + ======================================== + set and claim contracts, administrative + ======================================== + */ + + /** + * @dev Update this contract's token pointer to newContract (e.g. if the + * contract is upgraded) + */ + function setToken(TrueCurrency _newContract) external onlyOwner { + token = _newContract; + emit SetToken(_newContract); + } + + /** + * @dev Update this contract's registry pointer to _registry + */ + function setRegistry(Registry _registry) external onlyOwner { + registry = _registry; + emit SetRegistry(address(registry)); + } + + /** + * @dev Claim ownership of an arbitrary IHasOwner contract + */ + function issueClaimOwnership(address _other) public onlyOwner { + IHasOwner other = IHasOwner(_other); + other.claimOwnership(); + } + + /** + * @dev Transfer ownership of _child to _newOwner. + * Can be used e.g. to upgrade this TokenController contract. + * @param _child contract that tokenController currently Owns + * @param _newOwner new owner/pending owner of _child + */ + function transferChild(IHasOwner _child, address _newOwner) external onlyOwner { + _child.transferOwnership(_newOwner); + emit TransferChild(address(_child), _newOwner); + } + + /** + * @dev send all trx in token address to the owner of tokenController + */ + function requestReclaimTrx() external onlyOwner { + token.reclaimTrx(owner); + } + + /** + * @dev transfer all tokens of a particular type in token address to the + * owner of tokenController + * @param _token token address of the token to transfer + */ + function requestReclaimToken(ITRC20 _token) external onlyOwner { + token.reclaimToken(_token, owner); + } + + /** + * @dev pause all pausable actions on TrueCurrency, mints/burn/transfer/approve + */ + function pauseToken() external virtual onlyOwner { + OwnedUpgradeabilityProxy(address(uint160(address(token)))).upgradeTo(PAUSED_IMPLEMENTATION); + } + + /** + * @dev Change the minimum and maximum amounts that TrueCurrency users can + * burn to newMin and newMax + * @param _min minimum amount user can burn at a time + * @param _max maximum amount user can burn at a time + */ + function setBurnBounds(uint256 _min, uint256 _max) external onlyOwner { + token.setBurnBounds(_min, _max); + } + + /** + * @dev Owner can send trx balance in contract address + * @param _to address to which the funds will be send to + */ + function reclaimTrx(address payable _to) external onlyOwner { + _to.transfer(address(this).balance); + } + + /** + * @dev Owner can send trc20 token balance in contract address + * @param _token address of the token to send + * @param _to address to which the funds will be send to + */ + function reclaimToken(ITRC20 _token, address _to) external onlyOwner { + uint256 balance = _token.balanceOf(address(this)); + _token.transfer(_to, balance); + } + + /** + * @dev Owner can allow address to burn tokens + * @param burner address of the token that can burn + * @param canBurn true if account is allowed to burn, false otherwise + */ + function setCanBurn(address burner, bool canBurn) external onlyRegistryAdmin { + token.setCanBurn(burner, canBurn); + emit CanBurn(burner, canBurn); + } + + /** + * @dev Set blacklisted status for the account. + * @param account address to set blacklist flag for + * @param isBlacklisted blacklist flag value + */ + function setBlacklisted(address account, bool isBlacklisted) external onlyRegistryAdmin { + token.setBlacklisted(account, isBlacklisted); + } + +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/TronTrueUSD.sol b/packages/contracts-tron/contracts/TronTrueUSD.sol new file mode 100644 index 000000000..baa530653 --- /dev/null +++ b/packages/contracts-tron/contracts/TronTrueUSD.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {TrueCurrency} from "./common/TrueCurrency.sol"; + +/** + * @title TrueUSD + * @dev This is the top-level TRC20 contract, but most of the interesting functionality is + * inherited - see the documentation on the corresponding contracts. + */ +contract TrueUSD is TrueCurrency { + uint8 constant DECIMALS = 18; + uint8 constant ROUNDING = 2; + + function initialize() public { + require(!initialized, "already initialized"); + owner = msg.sender; + emit OwnershipTransferred(address(0), owner); + burnMin = 1000000000000000000000; + burnMax = 1000000000000000000000000000; + initialized = true; + } + + function decimals() public override pure returns (uint8) { + return DECIMALS; + } + + function rounding() public pure returns (uint8) { + return ROUNDING; + } + + function name() public override pure returns (string memory) { + return "TrueUSD"; + } + + function symbol() public override pure returns (string memory) { + return "TUSD"; + } +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/common/BurnableTokenWithBounds.sol b/packages/contracts-tron/contracts/common/BurnableTokenWithBounds.sol new file mode 100644 index 000000000..4f953b50e --- /dev/null +++ b/packages/contracts-tron/contracts/common/BurnableTokenWithBounds.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {ReclaimerToken} from "./ReclaimerToken.sol"; + +/** + * @title BurnableTokenWithBounds + * @dev Burning functions as redeeming money from the system. + * The platform will keep track of who burns coins, + * and will send them back the equivalent amount of money (rounded down to the nearest cent). + */ +abstract contract BurnableTokenWithBounds is ReclaimerToken { + /** + * @dev Emitted when `value` tokens are burnt from one account (`burner`) + * @param burner address which burned tokens + * @param value amount of tokens burned + */ + event Burn(address indexed burner, uint256 value); + + /** + * @dev Emitted when new burn bounds were set + * @param newMin new minimum burn amount + * @param newMax new maximum burn amount + * @notice `newMin` should never be greater than `newMax` + */ + event SetBurnBounds(uint256 newMin, uint256 newMax); + + /** + * @dev Destroys `amount` tokens from `msg.sender`, reducing the + * total supply. + * @param amount amount of tokens to burn + * + * Emits a {Transfer} event with `to` set to the zero address. + * Emits a {Burn} event with `burner` set to `msg.sender` + * + * Requirements + * + * - `msg.sender` must have at least `amount` tokens. + * + */ + function burn(uint256 amount) external { + _burn(msg.sender, amount); + } + + /** + * @dev Change the minimum and maximum amount that can be burned at once. + * Burning may be disabled by setting both to 0 (this will not be done + * under normal operation, but we can't add checks to disallow it without + * losing a lot of flexibility since burning could also be as good as disabled + * by setting the minimum extremely high, and we don't want to lock + * in any particular cap for the minimum) + * @param _min minimum amount that can be burned at once + * @param _max maximum amount that can be burned at once + */ + function setBurnBounds(uint256 _min, uint256 _max) external onlyOwner { + require(_min <= _max, "BurnableTokenWithBounds: min > max"); + burnMin = _min; + burnMax = _max; + emit SetBurnBounds(_min, _max); + } + + /** + * @dev Checks if amount is within allowed burn bounds and + * destroys `amount` tokens from `account`, reducing the + * total supply. + * @param account account to burn tokens for + * @param amount amount of tokens to burn + * + * Emits a {Burn} event + */ + function _burn(address account, uint256 amount) internal virtual override { + require(amount >= burnMin, "BurnableTokenWithBounds: below min burn bound"); + require(amount <= burnMax, "BurnableTokenWithBounds: exceeds max burn bound"); + + super._burn(account, amount); + emit Burn(account, amount); + } +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/common/ClaimableOwnable.sol b/packages/contracts-tron/contracts/common/ClaimableOwnable.sol new file mode 100644 index 000000000..7d5499ac9 --- /dev/null +++ b/packages/contracts-tron/contracts/common/ClaimableOwnable.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {ProxyStorage} from "./ProxyStorage.sol"; + +/** + * @title ClamableOwnable + * @dev The ClamableOwnable contract is a copy of Claimable Contract by Zeppelin. + * and provides basic authorization control functions. Inherits storage layout of + * ProxyStorage. + */ +contract ClaimableOwnable is ProxyStorage { + /** + * @dev emitted when ownership is transferred + * @param previousOwner previous owner of this contract + * @param newOwner new owner of this contract + */ + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev sets the original `owner` of the contract to the sender + * at construction. Must then be reinitialized + */ + constructor() public { + owner = msg.sender; + emit OwnershipTransferred(address(0), owner); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner, "only Owner"); + _; + } + + /** + * @dev Modifier throws if called by any account other than the pendingOwner. + */ + modifier onlyPendingOwner() { + require(msg.sender == pendingOwner, "only pending owner"); + _; + } + + /** + * @dev Allows the current owner to set the pendingOwner address. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + pendingOwner = newOwner; + } + + /** + * @dev Allows the pendingOwner address to finalize the transfer. + */ + function claimOwnership() public onlyPendingOwner { + emit OwnershipTransferred(owner, pendingOwner); + owner = pendingOwner; + pendingOwner = address(0); + } +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/common/ProxyStorage.sol b/packages/contracts-tron/contracts/common/ProxyStorage.sol new file mode 100644 index 000000000..4e6252e7c --- /dev/null +++ b/packages/contracts-tron/contracts/common/ProxyStorage.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +// solhint-disable max-states-count, var-name-mixedcase + +/** + * Defines the storage layout of the token implementation contract. Any + * newly declared state variables in future upgrades should be appended + * to the bottom. Never remove state variables from this list, however variables + * can be renamed. Please add _Deprecated to deprecated variables. + */ +contract ProxyStorage { + address public owner; + address public pendingOwner; + + bool initialized; + + uint256 _totalSupply; + + uint256 public burnMin = 0; + uint256 public burnMax = 0; + + mapping(address => uint256) _balances; + mapping(address => mapping(address => uint256)) _allowances; + + mapping(address => bool) isBlacklisted; + mapping(address => bool) public canBurn; + + /* Additionally, we have several keccak-based storage locations. + * If you add more keccak-based storage mappings, such as mappings, you must document them here. + * If the length of the keccak input is the same as an existing mapping, it is possible there could be a preimage collision. + * A preimage collision can be used to attack the contract by treating one storage location as another, + * which would always be a critical issue. + * Carefully examine future keccak-based storage to ensure there can be no preimage collisions. + ******************************************************************************************************* + ** length input usage + ******************************************************************************************************* + ** 19 "trueXXX.proxy.owner" Proxy Owner + ** 27 "trueXXX.pending.proxy.owner" Pending Proxy Owner + ** 28 "trueXXX.proxy.implementation" Proxy Implementation + ** 64 uint256(address),uint256(14) balanceOf + ** 64 uint256(address),keccak256(uint256(address),uint256(15)) allowance + ** 64 uint256(address),keccak256(bytes32,uint256(16)) attributes + **/ +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/common/ReclaimerToken.sol b/packages/contracts-tron/contracts/common/ReclaimerToken.sol new file mode 100644 index 000000000..40778ea81 --- /dev/null +++ b/packages/contracts-tron/contracts/common/ReclaimerToken.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {TRC20} from "./TRC20.sol"; +import {ITRC20} from "../interface/ITRC20.sol"; + +/** + * @title ReclaimerToken + * @dev TRC20 token which allows owner to reclaim TRC20 tokens + * or trx sent to this contract + */ +abstract contract ReclaimerToken is TRC20 { + /** + * @dev send all trx balance in the contract to another address + * @param _to address to send trx balance to + */ + function reclaimTrx(address payable _to) external onlyOwner { + _to.transfer(address(this).balance); + } + + /** + * @dev send all token balance of an arbitrary trc20 token + * in the contract to another address + * @param token token to reclaim + * @param _to address to send trc20 balance to + */ + function reclaimToken(ITRC20 token, address _to) external onlyOwner { + uint256 balance = token.balanceOf(address(this)); + token.transfer(_to, balance); + } +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/common/TRC20.sol b/packages/contracts-tron/contracts/common/TRC20.sol new file mode 100644 index 000000000..b76444df4 --- /dev/null +++ b/packages/contracts-tron/contracts/common/TRC20.sol @@ -0,0 +1,277 @@ +/** + * @notice This is a copy of openzeppelin ERC20 contract with removed state variables. + * Removing state variables has been necessary due to proxy pattern usage. + * Changes to Openzeppelin ERC20 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/de99bccbfd4ecd19d7369d01b070aa72c64423c9/contracts/token/ERC20/ERC20.sol: + * - Remove state variables _name, _symbol, _decimals + * - Use state variables _balances, _allowances, _totalSupply from ProxyStorage + * - Remove constructor + * - Contract made abstract + * + * See also: ClaimableOwnable.sol and ProxyStorage.sol + */ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {ITRC20} from "../interface/ITRC20.sol"; +import {Context} from "@openzeppelin/contracts/GSN/Context.sol"; +import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; +import {ClaimableOwnable} from "./ClaimableOwnable.sol"; + +/** + * @dev Implementation of the {ITRC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the TIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {ITRC20-approve}. + */ +abstract contract TRC20 is ClaimableOwnable, Context, ITRC20 { + using SafeMath for uint256; + + /** + * @dev Returns the name of the token. + */ + function name() public virtual pure returns (string memory); + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public virtual pure returns (string memory); + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18. This is the value {TRC20} uses, + * unless {_setupDecimals} is called. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {ITRC20-balanceOf} and {ITRC20-transfer}. + */ + function decimals() public virtual pure returns (uint8) { + return 18; + } + + /** + * @dev See {ITRC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {ITRC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {ITRC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {ITRC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {ITRC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {ITRC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the TIP. See the note at the beginning of {TRC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "TRC20: transfer amount exceeds allowance")); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {ITRC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {ITRC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "TRC20: decreased allowance below zero")); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "TRC20: transfer from the zero address"); + require(recipient != address(0), "TRC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "TRC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "TRC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "TRC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "TRC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. + * + * This is internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "TRC20: approve from the zero address"); + require(spender != address(0), "TRC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + // solhint-disable-next-line no-empty-blocks + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/common/TrueCurrency.sol b/packages/contracts-tron/contracts/common/TrueCurrency.sol new file mode 100644 index 000000000..8726943d4 --- /dev/null +++ b/packages/contracts-tron/contracts/common/TrueCurrency.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {BurnableTokenWithBounds} from "./BurnableTokenWithBounds.sol"; + +/** + * @title TrueCurrency + * @dev TrueCurrency is an TRC20 with blacklist & redemption addresses + * + * TrueCurrency is a compliant stablecoin with blacklist and redemption + * addresses. Only the owner can blacklist accounts. Redemption addresses + * are assigned automatically to the first 0x100000 addresses. Sending + * tokens to the redemption address will trigger a burn operation. Only + * the owner can mint or blacklist accounts. + * + * This contract is owned by the TokenController, which manages token + * minting & admin functionality. See TokenController.sol + * + * See also: BurnableTokenWithBounds.sol + * + * ~~~~ Features ~~~~ + * + * Redemption Addresses + * - The first 0x100000 addresses are redemption addresses + * - Tokens sent to redemption addresses are burned + * - Redemptions are tracked off-chain + * - Cannot mint tokens to redemption addresses + * + * Blacklist + * - Owner can blacklist accounts in accordance with local regulatory bodies + * - Only a court order will merit a blacklist; blacklisting is extremely rare + * + * Burn Bounds & CanBurn + * - Owner can set min & max burn amounts + * - Only accounts flagged in canBurn are allowed to burn tokens + * - canBurn prevents tokens from being sent to the incorrect address + * + * Reclaimer Token + * - TRC20 Tokens and Trx sent to this contract can be reclaimed by the owner + */ +abstract contract TrueCurrency is BurnableTokenWithBounds { + uint256 constant CENT = 10**16; + uint256 constant REDEMPTION_ADDRESS_COUNT = 0x100000; + + /** + * @dev Emitted when account blacklist status changes + */ + event Blacklisted(address indexed account, bool isBlacklisted); + + /** + * @dev Emitted when `value` tokens are minted for `to` + * @param to address to mint tokens for + * @param value amount of tokens to be minted + */ + event Mint(address indexed to, uint256 value); + + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * @param account address to mint tokens for + * @param amount amount of tokens to be minted + * + * Emits a {Mint} event + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` cannot be blacklisted. + * - `account` cannot be a redemption address. + */ + function mint(address account, uint256 amount) external onlyOwner { + require(!isBlacklisted[account], "TrueCurrency: account is blacklisted"); + require(!isRedemptionAddress(account), "TrueCurrency: account is a redemption address"); + _mint(account, amount); + emit Mint(account, amount); + } + + /** + * @dev Set blacklisted status for the account. + * @param account address to set blacklist flag for + * @param _isBlacklisted blacklist flag value + * + * Requirements: + * + * - `msg.sender` should be owner. + */ + function setBlacklisted(address account, bool _isBlacklisted) external onlyOwner { + require(uint256(account) >= REDEMPTION_ADDRESS_COUNT, "TrueCurrency: blacklisting of redemption address is not allowed"); + isBlacklisted[account] = _isBlacklisted; + emit Blacklisted(account, _isBlacklisted); + } + + /** + * @dev Set canBurn status for the account. + * @param account address to set canBurn flag for + * @param _canBurn canBurn flag value + * + * Requirements: + * + * - `msg.sender` should be owner. + */ + function setCanBurn(address account, bool _canBurn) external onlyOwner { + canBurn[account] = _canBurn; + } + + /** + * @dev Check if neither account is blacklisted before performing transfer + * If transfer recipient is a redemption address, burns tokens + * @notice Transfer to redemption address will burn tokens with a 1 cent precision + * @param sender address of sender + * @param recipient address of recipient + * @param amount amount of tokens to transfer + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual override { + require(!isBlacklisted[sender], "TrueCurrency: sender is blacklisted"); + require(!isBlacklisted[recipient], "TrueCurrency: recipient is blacklisted"); + + if (isRedemptionAddress(recipient)) { + super._transfer(sender, recipient, amount.sub(amount.mod(CENT))); + _burn(recipient, amount.sub(amount.mod(CENT))); + } else { + super._transfer(sender, recipient, amount); + } + } + + /** + * @dev Requere neither accounts to be blacklisted before approval + * @param owner address of owner giving approval + * @param spender address of spender to approve for + * @param amount amount of tokens to approve + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal override { + require(!isBlacklisted[owner], "TrueCurrency: tokens owner is blacklisted"); + require(!isBlacklisted[spender] || amount == 0, "TrueCurrency: tokens spender is blacklisted"); + + super._approve(owner, spender, amount); + } + + /** + * @dev Check if tokens can be burned at address before burning + * @param account account to burn tokens from + * @param amount amount of tokens to burn + */ + function _burn(address account, uint256 amount) internal override { + require(canBurn[account], "TrueCurrency: cannot burn from this address"); + super._burn(account, amount); + } + + /** + * @dev First 0x100000-1 addresses (0x0000000000000000000000000000000000000001 to 0x00000000000000000000000000000000000fffff) + * are the redemption addresses. + * @param account address to check is a redemption address + * + * All transfers to redemption address will trigger token burn. + * + * @notice For transfer to succeed, canBurn must be true for redemption address + * + * @return is `account` a redemption address + */ + function isRedemptionAddress(address account) internal pure returns (bool) { + return uint256(account) < REDEMPTION_ADDRESS_COUNT && uint256(account) != 0; + } +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/interface/ITRC20.sol b/packages/contracts-tron/contracts/interface/ITRC20.sol new file mode 100644 index 000000000..d52e78feb --- /dev/null +++ b/packages/contracts-tron/contracts/interface/ITRC20.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +/** + * @dev Interface of the TRC20 standard as defined in the TIP. + */ +interface ITRC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} \ No newline at end of file diff --git a/packages/contracts-tron/contracts/mocks/ForceEther.sol b/packages/contracts-tron/contracts/mocks/ForceEther.sol new file mode 100644 index 000000000..aeccde72a --- /dev/null +++ b/packages/contracts-tron/contracts/mocks/ForceEther.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +// Source: @openzeppelin/contracts/mocks/ForceEther.sol +// @title Force Ether into a contract. +// @notice even +// if the contract is not payable. +// @notice To use, construct the contract with the target as argument. +// @author Remco Bloemen +contract ForceEther { + // solhint-disable-next-line no-empty-blocks + constructor() public payable {} + + function destroyAndSend(address _recipient) public { + selfdestruct(address(uint160(_recipient))); + } +} diff --git a/packages/contracts-tron/contracts/mocks/MockTrueCurrency.sol b/packages/contracts-tron/contracts/mocks/MockTrueCurrency.sol new file mode 100644 index 000000000..11da296b4 --- /dev/null +++ b/packages/contracts-tron/contracts/mocks/MockTrueCurrency.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {TrueCurrency} from "../common/TrueCurrency.sol"; + +contract MockTrueCurrency is TrueCurrency { + uint8 constant DECIMALS = 18; + uint8 constant ROUNDING = 2; + + function initialize() external { + require(!initialized); + owner = msg.sender; + initialized = true; + } + + function decimals() public pure override returns (uint8) { + return DECIMALS; + } + + function rounding() public pure returns (uint8) { + return ROUNDING; + } + + function name() public pure override returns (string memory) { + return "TrueCurrency"; + } + + function symbol() public pure override returns (string memory) { + return "TCUR"; + } +} diff --git a/packages/contracts-tron/contracts/mocks/RegistryMock.sol b/packages/contracts-tron/contracts/mocks/RegistryMock.sol new file mode 100644 index 000000000..22239f7d4 --- /dev/null +++ b/packages/contracts-tron/contracts/mocks/RegistryMock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {Registry} from "../Registry.sol"; + +contract RegistryMock is Registry { + +} diff --git a/packages/contracts-tron/contracts/mocks/TokenControllerMock.sol b/packages/contracts-tron/contracts/mocks/TokenControllerMock.sol new file mode 100644 index 000000000..b48fe6b41 --- /dev/null +++ b/packages/contracts-tron/contracts/mocks/TokenControllerMock.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import {OwnedUpgradeabilityProxy} from "../OwnedUpgradeabilityProxy.sol"; +import {Registry} from "../Registry.sol"; + +import {TrueCurrency} from "../common/TrueCurrency.sol"; + +import {TokenController} from "../TronTokenController.sol"; + +interface HasOwner { + function claimOwnership() external; + + function transferOwnership(address newOwner) external; +} + +/** + * Token Controller with custom init function for testing + */ +contract TokenControllerMock is TokenController { + + // initialize with paramaters. useful for tests + // sets initial paramaters on testnet + function initializeWithParams(TrueCurrency _token, Registry _registry) external { + require(!initialized, "already initialized"); + owner = msg.sender; + initialized = true; + token = _token; + emit SetToken(_token); + registry = _registry; + emit SetRegistry(address(_registry)); + //gasRefunder = owner; + //registryAdmin = owner; + // set mint limits & thresholds + // instant = 1M, ratified = 10M, multisig = 100M + uint256 instant = 1000000000000000000000000; + uint256 ratified = 10000000000000000000000000; + uint256 multiSig = 100000000000000000000000000; + instantMintThreshold = instant; + ratifiedMintThreshold = ratified; + multiSigMintThreshold = multiSig; + instantMintLimit = instant; + ratifiedMintLimit = ratified; + multiSigMintLimit = multiSig; + instantMintPool = instant; + ratifiedMintPool = ratified; + multiSigMintPool = multiSig; + emit MintThresholdChanged(instant, ratified, multiSig); + emit MintLimitsChanged(instant, ratified, multiSig); + emit InstantPoolRefilled(); + emit RatifyPoolRefilled(); + emit MultiSigPoolRefilled(); + } + +} + +contract TokenControllerPauseMock is TokenControllerMock { + address public pausedImplementation; + + function setPausedImplementation(address _pausedToken) external { + pausedImplementation = _pausedToken; + } + + /** + * @dev pause all pausable actions on TrueUSD, mints/burn/transfer/approve + */ + function pauseToken() external override onlyOwner { + OwnedUpgradeabilityProxy(uint160(address(token))).upgradeTo(pausedImplementation); + } +} diff --git a/packages/contracts-tron/hardhat.config.ts b/packages/contracts-tron/hardhat.config.ts new file mode 100644 index 000000000..2acbed4fa --- /dev/null +++ b/packages/contracts-tron/hardhat.config.ts @@ -0,0 +1,30 @@ +import '@typechain/hardhat' +import '@nomiclabs/hardhat-waffle' +import './abi-exporter' + +import compiler from './.compiler.json' + +module.exports = { + paths: { + sources: './contracts', + artifacts: './build', + cache: './cache', + }, + abiExporter: { + path: './build', + flat: true, + spacing: 2, + }, + networks: { + hardhat: { + allowUnlimitedContractSize: true, + }, + }, + typechain: { + outDir: 'build/types', + target: 'ethers-v5', + }, + solidity: { + compilers: [compiler], + }, +} diff --git a/packages/contracts-tron/package.json b/packages/contracts-tron/package.json new file mode 100644 index 000000000..2274f9081 --- /dev/null +++ b/packages/contracts-tron/package.json @@ -0,0 +1,49 @@ +{ + "name": "@trusttoken-smart-contracts/contracts-tron", + "version": "0.0.1", + "description": "TrueUSD contract on Tron", + "private": true, + "scripts": { + "clean": "rm -rf ./build && hardhat clean", + "lint": "yarn lint:sol && yarn lint:ts", + "typecheck": "tsc --noEmit", + "lint:sol": "solhint 'contracts/**/*.sol' && prettylint 'contracts/**/*.sol'", + "lint:ts": "eslint '{test,scripts}/**/*.ts' -c .eslintrc.typescript.js", + "lint:fix": "prettier 'contracts/**/*.sol' --write --loglevel error && yarn lint:ts --fix", + "prebuild": "yarn clean", + "build:hardhat": "hardhat compile", + "build:typechain": "typechain --target ethers-v5 --out-dir build/types 'build/*.json'", + "build": "yarn build:hardhat && yarn build:typechain && mars", + "preflatten": "rm -rf custom_flatten", + "flatten": "waffle flatten .waffle.json", + "test": "mocha 'test/**/*.test.ts'", + "checks": "yarn lint && yarn test" + }, + "dependencies": { + "ethereum-mars": "0.2.5", + "@openzeppelin/contracts": "3.4.2", + "@chainlink/contracts": "0.1.6" + }, + "devDependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/providers": "^5.7.0", + "@nomiclabs/hardhat-waffle": "^2.0.3", + "@typechain/ethers-v5": "^10.0.0", + "@typechain/hardhat": "^6.0.0", + "@types/chai": "^4.3.3", + "@types/mocha": "^9.1.1", + "@types/node": "^17.0.34", + "chai": "^4.3.6", + "ethers": "^5.7.0", + "hardhat": "~2.10.2", + "mocha": "^10.0.0", + "ts-node": "^10.7.0", + "tsconfig-paths": "^4.1.0", + "typechain": "^8.0.0", + "typescript": "4.5.4", + "solhint": "^3.0.0", + "prettylint": "^1.0.0", + "prettier": "^2.4.1" + } +} diff --git a/packages/contracts-tron/test/ProxyWithController.test.ts b/packages/contracts-tron/test/ProxyWithController.test.ts new file mode 100644 index 000000000..f29b8ff4b --- /dev/null +++ b/packages/contracts-tron/test/ProxyWithController.test.ts @@ -0,0 +1,129 @@ +import { expect, use } from 'chai' +import { solidity } from 'ethereum-waffle' +import { Wallet } from 'ethers' +import { parseEther } from '@ethersproject/units' +import { formatBytes32String } from '@ethersproject/strings' + +import { beforeEachWithFixture } from 'fixtures/beforeEachWithFixture' + +import { + RegistryMock, + MockTrueCurrency, + OwnedUpgradeabilityProxy, + TokenControllerMock, + RegistryMock__factory, + MockTrueCurrency__factory, + OwnedUpgradeabilityProxy__factory, + TokenControllerMock__factory, +} from 'contracts' + +use(solidity) + +describe('ProxyWithController', () => { + let owner: Wallet + let otherWallet: Wallet + let thirdWallet: Wallet + let mintKey: Wallet + let pauseKey: Wallet + let approver1: Wallet + let approver2: Wallet + let approver3: Wallet + + let registry: RegistryMock + + let tokenProxy: OwnedUpgradeabilityProxy + let tusdImplementation: MockTrueCurrency + let token: MockTrueCurrency + + let controllerProxy: OwnedUpgradeabilityProxy + let controllerImplementation: TokenControllerMock + let controller: TokenControllerMock + + const notes = formatBytes32String('some notes') + const CAN_BURN = formatBytes32String('canBurn') + + beforeEachWithFixture(async (wallets) => { + [owner, otherWallet, thirdWallet, mintKey, pauseKey, approver1, approver2, approver3] = wallets + registry = await new RegistryMock__factory(owner).deploy() + + tokenProxy = await new OwnedUpgradeabilityProxy__factory(owner).deploy() + tusdImplementation = await new MockTrueCurrency__factory(owner).deploy() + await tokenProxy.upgradeTo(tusdImplementation.address) + token = new MockTrueCurrency__factory(owner).attach(tokenProxy.address) + await token.initialize() + + controllerProxy = await new OwnedUpgradeabilityProxy__factory(owner).deploy() + controllerImplementation = await new TokenControllerMock__factory(owner).deploy() + await controllerProxy.upgradeTo(controllerImplementation.address) + controller = new TokenControllerMock__factory(owner).attach(controllerProxy.address) + + await controller.initialize() + await controller.setToken(token.address) + await controller.transferMintKey(mintKey.address) + }) + + describe('Set up proxy', () => { + it('controller cannot be reinitialized', async () => { + await expect(controller.initialize()) + .to.be.reverted + }) + + it('owner can transfer ownership to pending owner', async () => { + await controller.transferOwnership(otherWallet.address) + }) + + it('non owner cannot transfer ownership', async () => { + await expect(controller.connect(otherWallet).transferOwnership(otherWallet.address)) + .to.be.reverted + }) + + it('pending owner can claim ownerhship', async () => { + await controller.transferOwnership(otherWallet.address) + await controller.connect(otherWallet).claimOwnership() + }) + + it('non pending owner cannot claim ownership', async () => { + await controller.transferOwnership(otherWallet.address) + await expect(controller.connect(thirdWallet).claimOwnership()) + .to.be.reverted + }) + + it('token can transfer ownership to controller', async () => { + await token.transferOwnership(controller.address) + expect(await token.pendingOwner()).to.equal(controller.address) + + await controller.issueClaimOwnership(token.address) + expect(await token.owner()).to.equal(controller.address) + }) + + describe('TokenController functions', () => { + beforeEach(async function () { + await registry.setAttribute(thirdWallet.address, CAN_BURN, 1, notes) + await registry.setAttribute(approver1.address, formatBytes32String('isTUSDMintApprover'), 1, notes) + await registry.setAttribute(approver2.address, formatBytes32String('isTUSDMintApprover'), 1, notes) + await registry.setAttribute(approver3.address, formatBytes32String('isTUSDMintApprover'), 1, notes) + await registry.setAttribute(pauseKey.address, formatBytes32String('isTUSDMintPausers'), 1, notes) + await token.mint(thirdWallet.address, parseEther('1000')) + await token.transferOwnership(controller.address) + await controller.issueClaimOwnership(token.address) + await controller.setMintThresholds(parseEther('100'), parseEther('1000'), parseEther('10000')) + await controller.setMintLimits(parseEther('300'), parseEther('3000'), parseEther('30000')) + }) + + it('non mintKey/owner cannot request mint', async () => { + await expect(controller.connect(otherWallet).requestMint(otherWallet.address, parseEther('100'))) + .to.be.reverted + }) + + it('request a mint', async () => { + expect(await controller.mintOperationCount()).to.equal(0) + await controller.requestMint(thirdWallet.address, parseEther('100')) + const mintOperation = await controller.mintOperations(0) + expect(mintOperation[0]).to.equal(thirdWallet.address) + expect(mintOperation[1]).to.equal(parseEther('100')) + expect(mintOperation[3]).to.equal(0) + expect(await controller.mintOperationCount()).to.equal(1) + }) + }) + }) +}) diff --git a/packages/contracts-tron/test/TokenController.test.ts b/packages/contracts-tron/test/TokenController.test.ts new file mode 100644 index 000000000..62bcf2703 --- /dev/null +++ b/packages/contracts-tron/test/TokenController.test.ts @@ -0,0 +1,602 @@ +import { expect, use } from 'chai' +import { BigNumberish, Wallet } from 'ethers' +import { MockProvider, solidity } from 'ethereum-waffle' +import { formatBytes32String } from '@ethersproject/strings' +import { parseEther } from '@ethersproject/units' + +import { beforeEachWithFixture } from 'fixtures/beforeEachWithFixture' + +import { + TokenControllerMock__factory, + TokenControllerMock, + RegistryMock, + RegistryMock__factory, + OwnedUpgradeabilityProxy, + OwnedUpgradeabilityProxy__factory, + MockTrueCurrency__factory, + MockTrueCurrency, + ForceEther, + ForceEther__factory, +} from 'contracts' + +use(solidity) + +describe('TokenController', () => { + let provider: MockProvider + + let owner: Wallet + let otherWallet: Wallet + let thirdWallet: Wallet + let mintKey: Wallet + let pauseKey: Wallet + let ratifier1: Wallet + let ratifier2: Wallet + let ratifier3: Wallet + + let token: MockTrueCurrency + let tokenImplementation: MockTrueCurrency + let tokenProxy: OwnedUpgradeabilityProxy + let controller: TokenControllerMock + let registry: RegistryMock + + const notes = formatBytes32String('notes') + const CAN_BURN = formatBytes32String('canBurn') + + const expectTokenBalance = async (token: MockTrueCurrency, wallet: Wallet, value: BigNumberish) => { + expect(await token.balanceOf(wallet.address)).to.equal(value) + } + + beforeEachWithFixture(async (wallets, _provider) => { + [owner, otherWallet, thirdWallet, mintKey, pauseKey, ratifier1, ratifier2, ratifier3] = wallets + provider = _provider + + registry = await new RegistryMock__factory(owner).deploy() + controller = await new TokenControllerMock__factory(owner).deploy() + await controller.initialize() + + tokenProxy = await new OwnedUpgradeabilityProxy__factory(owner).deploy() + tokenImplementation = await new MockTrueCurrency__factory(owner).deploy() + await tokenProxy.upgradeTo(tokenImplementation.address) + + token = new MockTrueCurrency__factory(owner).attach(tokenProxy.address) + await token.initialize() + + await token.transferOwnership(controller.address) + await controller.issueClaimOwnership(token.address) + await controller.setRegistry(registry.address) + await controller.setToken(token.address) + await controller.transferMintKey(mintKey.address) + await tokenProxy.transferProxyOwnership(controller.address) + await controller.claimTrueCurrencyProxyOwnership() + await registry.setAttribute(thirdWallet.address, CAN_BURN, 1, notes, { gasLimit: 5_000_000 }) + await registry.setAttribute(ratifier1.address, formatBytes32String('isTUSDMintRatifier'), 1, notes, { gasLimit: 5_000_000 }) + await registry.setAttribute(ratifier2.address, formatBytes32String('isTUSDMintRatifier'), 1, notes, { gasLimit: 5_000_000 }) + await registry.setAttribute(ratifier3.address, formatBytes32String('isTUSDMintRatifier'), 1, notes, { gasLimit: 5_000_000 }) + await registry.setAttribute(pauseKey.address, formatBytes32String('isTUSDMintPausers'), 1, notes) + await controller.setMintThresholds(parseEther('200'), parseEther('1000'), parseEther('1001')) + await controller.setMintLimits(parseEther('200'), parseEther('300'), parseEther('3000')) + await controller.refillMultiSigMintPool() + await controller.refillRatifiedMintPool() + await controller.refillInstantMintPool() + await controller.instantMint(thirdWallet.address, parseEther('100')) + await controller.setMintLimits(0, 0, 0) + }) + + describe('Request and Finalize Mints (owner)', () => { + beforeEach(async function () { + await controller.setMintThresholds(parseEther('10'), parseEther('100'), parseEther('1000')) + await controller.setMintLimits(parseEther('30'), parseEther('300'), parseEther('3000')) + }) + + it('mint limits cannot be out of order', async () => { + await expect(controller.setMintLimits(parseEther('300'), parseEther('30'), parseEther('3000'))) + .to.be.reverted + await expect(controller.setMintLimits(parseEther('30'), parseEther('300'), parseEther('200'))) + .to.be.reverted + }) + + it('mint thresholds cannot be out of order', async () => { + await expect(controller.setMintThresholds(parseEther('100'), parseEther('10'), parseEther('1000'))) + .to.be.reverted + await expect(controller.setMintThresholds(parseEther('10'), parseEther('100'), parseEther('50'))) + .to.be.reverted + }) + + it('non mintKey/owner cannot request mint', async () => { + await expect(controller.connect(otherWallet).requestMint(thirdWallet.address, parseEther('10'))) + .to.be.reverted + }) + + it('request a mint', async () => { + expect(await controller.mintOperationCount()).to.equal(0) + await controller.requestMint(thirdWallet.address, parseEther('10')) + const mintOperation = await controller.mintOperations(0) + expect(mintOperation[0]).to.equal(thirdWallet.address) + expect(mintOperation[1]).to.equal(parseEther('10')) + expect(mintOperation[3]).to.equal(0) + expect(await controller.mintOperationCount()).to.equal(1) + }) + + it('request mint then revoke it', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + + await expect(controller.connect(mintKey).revokeMint(0)) + .to.emit(controller, 'RevokeMint') + + const mintOperation = await controller.mintOperations(0) + expect(mintOperation[0]).to.equal('0x0000000000000000000000000000000000000000') + expect(mintOperation[1]).to.equal('0') + expect(mintOperation[2]).to.equal('0') + expect(mintOperation[3]).to.equal('0') + }) + + it('request and finalize a mint', async () => { + await controller.requestMint(thirdWallet.address, parseEther('10')) + await expect(controller.ratifyMint(0, thirdWallet.address, parseEther('10'))) + .to.emit(controller, 'MintRatified') + .withArgs('0', owner.address) + + expect(await token.totalSupply()).to.equal(parseEther('110')) + }) + + it('fails to transfer mintkey to 0x0', async () => { + await expect(controller.transferMintKey('0x0000000000000000000000000000000000000000')) + .to.be.reverted + }) + + it('non owner/mintkey cannot transfer mintkey', async () => { + await expect(controller.connect(otherWallet).transferMintKey(thirdWallet.address)) + .to.be.reverted + }) + }) + + describe('Emit Proper Event Logs', async () => { + it('transfer mintkey should generate logs', async () => { + await expect(controller.transferMintKey(thirdWallet.address)) + .to.emit(controller, 'TransferMintKey') + .withArgs(mintKey.address, thirdWallet.address) + }) + + it('pause mint should generate logs', async () => { + await expect(controller.pauseMints()) + .to.emit(controller, 'AllMintsPaused') + .withArgs(true) + }) + + it('changing mint thresholds should generate logs', async () => { + await expect(controller.setMintThresholds(parseEther('10'), parseEther('100'), parseEther('1000'))) + .to.emit(controller, 'MintThresholdChanged') + .withArgs(parseEther('10'), parseEther('100'), parseEther('1000')) + }) + + it('changing mint limits should generate logs', async () => { + await expect(controller.setMintLimits(parseEther('30'), parseEther('300'), parseEther('3000'))) + .to.emit(controller, 'MintLimitsChanged') + .withArgs(parseEther('30'), parseEther('300'), parseEther('3000')) + }) + }) + + describe('Full mint process', () => { + beforeEach(async function () { + await controller.setMintThresholds(parseEther('10'), parseEther('100'), parseEther('1000')) + await controller.setMintLimits(parseEther('30'), parseEther('300'), parseEther('3000')) + await controller.refillMultiSigMintPool() + await controller.refillRatifiedMintPool() + await controller.refillInstantMintPool() + }) + + it('have enough approvals for mints', async () => { + expect(await controller.connect(otherWallet).hasEnoughApproval(1, parseEther('50'))).to.be.true + expect(await controller.connect(otherWallet).hasEnoughApproval(1, parseEther('200'))).to.be.false + expect(await controller.connect(otherWallet).hasEnoughApproval(3, parseEther('200'))).to.be.true + expect(await controller.connect(otherWallet).hasEnoughApproval(3, parseEther('2000'))).to.be.false + expect(await controller.connect(otherWallet).hasEnoughApproval(2, parseEther('500'))).to.be.false + expect(await controller.connect(otherWallet).hasEnoughApproval(0, parseEther('50'))).to.be.false + }) + + it('owner can finalize before without approvals', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await controller.ratifyMint(0, thirdWallet.address, parseEther('10')) + }) + + it('non ratifiers cannot ratify mints', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await expect(controller.connect(otherWallet).ratifyMint(0, thirdWallet.address, parseEther('10'))) + .to.be.reverted + }) + + it('ratifier cannot ratify twice', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('200')) + await controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('200')) + await expect(controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('200'))) + .to.be.reverted + }) + + it('ratify mint should generate logs', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await expect(controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('10'))) + .to.emit(controller, 'MintRatified') + .withArgs(0, ratifier1.address) + }) + + it('cannot approve the same mint twice', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('10')) + await expect(controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('10'))) + .to.be.reverted + }) + + it('cannot request mint when mint paused', async () => { + await controller.connect(pauseKey).pauseMints() + await expect(controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10'))) + .to.be.reverted + }) + + it('non pause key cannot pause mint', async () => { + await expect(controller.connect(otherWallet).pauseMints()) + .to.be.reverted + }) + + it('pause key cannot unpause', async () => { + await expect(controller.connect(pauseKey).unpauseMints()) + .to.be.reverted + }) + + it('owner pauses then unpause then mints', async () => { + await controller.pauseMints() + await controller.unpauseMints() + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + }) + + it('ratify fails when the amount does not match', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await expect(controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('11'))) + .to.be.reverted + }) + + it('ratify fails when the to address does not match', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await expect(controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('10'))) + .to.be.reverted + }) + + it('instant mint a small amount', async () => { + await controller.connect(mintKey).instantMint(otherWallet.address, parseEther('10')) + await expectTokenBalance(token, otherWallet, parseEther('10')) + }) + + it('cannot instant mint over the instant mint threshold', async () => { + await expect(controller.connect(mintKey).instantMint(otherWallet.address, parseEther('15'))) + .to.be.reverted + }) + + it('cannot instant when the instant mint pool is dry', async () => { + await controller.connect(mintKey).instantMint(otherWallet.address, parseEther('10')) + await controller.connect(mintKey).instantMint(otherWallet.address, parseEther('10')) + await controller.connect(mintKey).instantMint(otherWallet.address, parseEther('8')) + await expect(controller.connect(mintKey).instantMint(otherWallet.address, parseEther('5'))) + .to.be.reverted + }) + + it('does the entire ratify mint process', async () => { + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('20')) + await controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('20')) + await expectTokenBalance(token, otherWallet, parseEther('20')) + expect(await controller.ratifiedMintPool()).to.equal(parseEther('250')) + }) + + it('single approval ratify does not finalize if over the ratifiedMintthreshold', async () => { + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('200')) + await controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('200')) + await expectTokenBalance(token, otherWallet, 0) + }) + + it('single approval ratify mint does not finalize if over the ratifiedMintPool is dry', async () => { + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('100')) + await controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('100')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('100')) + await controller.connect(ratifier1).ratifyMint(1, otherWallet.address, parseEther('100')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('30')) + await controller.connect(ratifier1).ratifyMint(2, otherWallet.address, parseEther('30')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('50')) + await controller.connect(ratifier1).ratifyMint(3, otherWallet.address, parseEther('50')) + await expectTokenBalance(token, otherWallet, parseEther('230')) + }) + + it('cannot finalize mint without enough approvers', async () => { + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('50')) + await expect(controller.connect(mintKey).finalizeMint(0)) + .to.be.reverted + await controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('50')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('500')) + await controller.connect(ratifier1).ratifyMint(1, otherWallet.address, parseEther('500')) + await controller.connect(ratifier2).ratifyMint(1, otherWallet.address, parseEther('500')) + await expect(controller.connect(mintKey).finalizeMint(1)) + .to.be.reverted + await controller.connect(ratifier3).ratifyMint(1, otherWallet.address, parseEther('500')) + }) + + it('owner can finalize mint without ratifiers', async () => { + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('50')) + await controller.finalizeMint(0) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('500')) + await controller.finalizeMint(1) + }) + + it('does the entire multiSig mint process', async () => { + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('200')) + await controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('200')) + await controller.connect(ratifier2).ratifyMint(0, otherWallet.address, parseEther('200')) + await controller.connect(ratifier3).ratifyMint(0, otherWallet.address, parseEther('200')) + await expectTokenBalance(token, otherWallet, parseEther('200')) + expect(await controller.multiSigMintPool()).to.equal(parseEther('2500')) + }) + + it('multiSig mint does not finalize if over the jumbpMintthreshold', async () => { + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('2000')) + await controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('2000')) + await controller.connect(ratifier2).ratifyMint(0, otherWallet.address, parseEther('2000')) + await controller.connect(ratifier3).ratifyMint(0, otherWallet.address, parseEther('2000')) + await expectTokenBalance(token, otherWallet, 0) + }) + + it('multiSig mint does not finalize if over the multiSigMintPool is dry', async () => { + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('1000')) + await controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('1000')) + await controller.connect(ratifier2).ratifyMint(0, otherWallet.address, parseEther('1000')) + await controller.connect(ratifier3).ratifyMint(0, otherWallet.address, parseEther('1000')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('1000')) + await controller.connect(ratifier1).ratifyMint(1, otherWallet.address, parseEther('1000')) + await controller.connect(ratifier2).ratifyMint(1, otherWallet.address, parseEther('1000')) + await controller.connect(ratifier3).ratifyMint(1, otherWallet.address, parseEther('1000')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('300')) + await controller.connect(ratifier1).ratifyMint(2, otherWallet.address, parseEther('300')) + await controller.connect(ratifier2).ratifyMint(2, otherWallet.address, parseEther('300')) + await controller.connect(ratifier3).ratifyMint(2, otherWallet.address, parseEther('300')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('500')) + await controller.connect(ratifier1).ratifyMint(3, otherWallet.address, parseEther('500')) + await controller.connect(ratifier2).ratifyMint(3, otherWallet.address, parseEther('500')) + await controller.connect(ratifier3).ratifyMint(3, otherWallet.address, parseEther('500')) + await expectTokenBalance(token, otherWallet, parseEther('2300')) + }) + + it('owner can mint unlimited amount', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10000')) + await controller.ratifyMint(0, thirdWallet.address, parseEther('10000')) + }) + + it('pause key can pause specific mint', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await controller.connect(pauseKey).pauseMint(0) + await expect(controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('10'))) + .to.be.reverted + }) + + it('pause key cannot unpause specific mint', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await controller.connect(pauseKey).pauseMint(0) + await expect(controller.connect(pauseKey).unpauseMint(0)) + .to.be.reverted + }) + + it('owner can unpause specific mint', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await controller.connect(pauseKey).pauseMint(0) + await controller.unpauseMint(0) + await controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('10')) + }) + + it('cannot finalize after all request invalidated', async () => { + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await controller.connect(mintKey).requestMint(thirdWallet.address, parseEther('10')) + await controller.invalidateAllPendingMints() + await expect(controller.connect(ratifier1).ratifyMint(0, thirdWallet.address, parseEther('10'))) + .to.be.reverted + await expect(controller.connect(ratifier1).ratifyMint(1, thirdWallet.address, parseEther('10'))) + .to.be.reverted + }) + }) + + describe('refill mint pool', function () { + beforeEach(async function () { + await controller.setMintThresholds(parseEther('10'), parseEther('100'), parseEther('1000')) + await controller.setMintLimits(parseEther('30'), parseEther('300'), parseEther('3000')) + }) + + it('refills multiSig mint pool', async function () { + await expect(controller.refillMultiSigMintPool()) + .to.emit(controller, 'MultiSigPoolRefilled') + expect(await controller.multiSigMintPool()).to.equal(parseEther('3000')) + }) + + it('refills ratify mint pool', async function () { + await controller.refillMultiSigMintPool() + await controller.connect(ratifier1).refillRatifiedMintPool() + await controller.connect(ratifier2).refillRatifiedMintPool() + + await expect(controller.connect(ratifier3).refillRatifiedMintPool()) + .to.emit(controller, 'RatifyPoolRefilled') + expect(await controller.ratifiedMintPool()).to.equal(parseEther('300')) + expect(await controller.multiSigMintPool()).to.equal(parseEther('2700')) + }) + + it('refills instant mint pool', async function () { + await controller.refillMultiSigMintPool() + await controller.refillRatifiedMintPool() + + await expect(controller.refillInstantMintPool()) + .to.emit(controller, 'InstantPoolRefilled') + expect(await controller.ratifiedMintPool()).to.equal(parseEther('270')) + expect(await controller.multiSigMintPool()).to.equal(parseEther('2700')) + expect(await controller.instantMintPool()).to.equal(parseEther('30')) + }) + + it('Ratifier cannot refill RatifiedMintPool alone', async function () { + await controller.refillMultiSigMintPool() + await controller.connect(ratifier1).refillRatifiedMintPool() + await controller.connect(ratifier2).refillRatifiedMintPool() + await expect(controller.connect(ratifier1).refillRatifiedMintPool()) + .to.be.reverted + await expect(controller.connect(ratifier2).refillRatifiedMintPool()) + .to.be.reverted + }) + + it('refilling the ratify pool clears the array', async function () { + await controller.refillMultiSigMintPool() + await controller.connect(ratifier1).refillRatifiedMintPool() + await controller.connect(ratifier2).refillRatifiedMintPool() + await controller.connect(ratifier3).refillRatifiedMintPool() + await controller.connect(ratifier1).refillRatifiedMintPool() + await controller.connect(ratifier2).refillRatifiedMintPool() + }) + + it('can finalize mint after refill', async function () { + await controller.refillMultiSigMintPool() + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('1000')) + await controller.connect(ratifier1).ratifyMint(0, otherWallet.address, parseEther('1000')) + await controller.connect(ratifier2).ratifyMint(0, otherWallet.address, parseEther('1000')) + await controller.connect(ratifier3).ratifyMint(0, otherWallet.address, parseEther('1000')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('1000')) + await controller.connect(ratifier1).ratifyMint(1, otherWallet.address, parseEther('1000')) + await controller.connect(ratifier2).ratifyMint(1, otherWallet.address, parseEther('1000')) + await controller.connect(ratifier3).ratifyMint(1, otherWallet.address, parseEther('1000')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('800')) + await controller.connect(ratifier1).ratifyMint(2, otherWallet.address, parseEther('800')) + await controller.connect(ratifier2).ratifyMint(2, otherWallet.address, parseEther('800')) + await controller.connect(ratifier3).ratifyMint(2, otherWallet.address, parseEther('800')) + await controller.connect(mintKey).requestMint(otherWallet.address, parseEther('500')) + await controller.connect(ratifier1).ratifyMint(3, otherWallet.address, parseEther('500')) + await controller.connect(ratifier2).ratifyMint(3, otherWallet.address, parseEther('500')) + await controller.connect(ratifier3).ratifyMint(3, otherWallet.address, parseEther('500')) + await expectTokenBalance(token, otherWallet, parseEther('2800')) + await controller.refillMultiSigMintPool() + await controller.connect(mintKey).finalizeMint(3) + await expectTokenBalance(token, otherWallet, parseEther('3300')) + }) + }) + + describe('initialization', function () { + it('controller cannot be re-initialized', async function () { + await expect(controller.initialize()) + .to.be.reverted + }) + }) + + describe('transfer child', function () { + it('can transfer trueUSD ownership to another address', async function () { + await controller.transferChild(token.address, owner.address) + expect(await token.pendingOwner()).to.equal(owner.address) + }) + }) + + describe('setBurnBounds', function () { + it('sets burnBounds', async function () { + await controller.setBurnBounds(parseEther('3'), parseEther('4')) + + expect(await token.burnMin()) + .to.equal(parseEther('3')) + expect(await token.burnMax()) + .to.equal(parseEther('4')) + }) + + it('cannot be called by non Owner', async function () { + await expect(controller.connect(otherWallet).setBurnBounds(parseEther('3'), parseEther('4'))) + .to.be.reverted + }) + + it('cannot be called by non Owner', async function () { + await expect(controller.connect(otherWallet).setBurnBounds(parseEther('3'), parseEther('4'))) + .to.be.reverted + }) + }) + + describe('pause trueUSD and wipe accounts', function () { + it('TokenController can pause TrueUSD transfers', async function () { + await token.connect(thirdWallet).transfer(mintKey.address, parseEther('10')) + await controller.pauseToken() + expect(await tokenProxy.implementation()) + .to.equal('0x9821722854922f004a9EE24d8aedF3D422745062') + }) + + it('non pauser cannot pause TrueUSD ', async function () { + await expect(controller.connect(mintKey).pauseToken()) + .to.be.reverted + }) + }) + + describe('fall back function', function () { + it('controller does not accept ether', async function () { + await expect(thirdWallet.sendTransaction({ to: controller.address, gasLimit: 6000000, value: 10 })) + .to.be.reverted + }) + }) + + describe('requestReclaimTrx', function () { + let forceEther: ForceEther + + beforeEach(async () => { + forceEther = await new ForceEther__factory(thirdWallet).deploy({ value: parseEther('1') }) + }) + + it('reclaims ether', async function () { + await forceEther.destroyAndSend(token.address) + const balance1 = await provider.getBalance(owner.address) + await controller.requestReclaimTrx() + const balance2 = await provider.getBalance(owner.address) + expect(balance2.gt(balance1)).to.be.true + }) + + it('cannot be called by non-owner', async function () { + await forceEther.destroyAndSend(token.address) + await expect(controller.connect(otherWallet).requestReclaimTrx()) + .to.be.reverted + }) + }) + + describe('requestReclaimToken', function () { + it('reclaims token', async function () { + await token.connect(thirdWallet).transfer(token.address, parseEther('40')) + await controller.requestReclaimToken(token.address) + await expectTokenBalance(token, owner, parseEther('40')) + }) + + it('cannot be called by non-owner', async function () { + await token.connect(thirdWallet).transfer(token.address, parseEther('40')) + await expect(controller.connect(otherWallet).requestReclaimToken(token.address)) + .to.be.reverted + }) + + it('can reclaim token in the controller contract address', async function () { + await token.connect(thirdWallet).transfer(controller.address, parseEther('40')) + await controller.reclaimToken(token.address, owner.address) + await expectTokenBalance(token, owner, parseEther('40')) + }) + }) + + describe('setCanBurn', () => { + it('sets whether address can burn', async () => { + await controller.connect(owner).setCanBurn(otherWallet.address, true) + expect(await token.canBurn(otherWallet.address)).to.be.true + await controller.connect(owner).setCanBurn(otherWallet.address, false) + expect(await token.canBurn(otherWallet.address)).to.be.false + }) + + it('rejects when called by non owner or registry admin', async () => { + await expect(controller.connect(otherWallet).setCanBurn(otherWallet.address, true)) + .to.be.revertedWith('must be registry admin or owner') + }) + }) + + describe('setBlacklisted', () => { + it('sets blacklisted status for the account', async () => { + await expect(controller.setBlacklisted(otherWallet.address, true)).to.emit(token, 'Blacklisted') + .withArgs(otherWallet.address, true) + await expect(controller.setBlacklisted(otherWallet.address, false)).to.emit(token, 'Blacklisted') + .withArgs(otherWallet.address, false) + }) + + it('rejects when called by non owner', async () => { + await expect(controller.connect(otherWallet).setBlacklisted(otherWallet.address, true)).to.be.revertedWith('only Owner') + }) + }) +}) diff --git a/packages/contracts-tron/test/fixtures/beforeEachWithFixture.ts b/packages/contracts-tron/test/fixtures/beforeEachWithFixture.ts new file mode 100644 index 000000000..deeab488a --- /dev/null +++ b/packages/contracts-tron/test/fixtures/beforeEachWithFixture.ts @@ -0,0 +1,14 @@ +import { waffle } from 'hardhat' +import type { MockProvider } from 'ethereum-waffle' +import type { Wallet } from 'ethers' + +export type Fixture = (wallets: Wallet[], provider: MockProvider) => Promise; + +export const loadFixture = waffle.createFixtureLoader( + waffle.provider.getWallets(), + waffle.provider, +) + +export function beforeEachWithFixture(fixture: Fixture) { + beforeEach(() => loadFixture(fixture)) +} diff --git a/packages/contracts-tron/test/setup.ts b/packages/contracts-tron/test/setup.ts new file mode 100644 index 000000000..6b3079fbf --- /dev/null +++ b/packages/contracts-tron/test/setup.ts @@ -0,0 +1,50 @@ +import { Fixture, MockProvider } from 'ethereum-waffle' +import { waffle } from 'hardhat' +import { Wallet } from 'ethers' +import './utils/hardhatPatches.ts' + +type FixtureLoader = ReturnType +interface FixtureReturns { + provider: MockProvider, + wallet: Wallet, + other: Wallet, + another: Wallet, +} + +let loadFixture: ReturnType | undefined +export function setupFixtureLoader() { + if (!loadFixture) { + loadFixture = setupOnce() + } + return loadFixture +} + +type CurrentLoader = { loader: FixtureLoader, returns: FixtureReturns, fixture: Fixture } + +function setupOnce() { + let currentLoader: CurrentLoader = { + loader: {} as FixtureLoader, + returns: {} as FixtureReturns, + fixture: {} as Fixture, + } + + async function makeLoader(): Promise<{ loader: FixtureLoader, returns: FixtureReturns }> { + const { provider } = waffle + await provider.send('hardhat_reset', []) + const [wallet, other, another, ...rest] = provider.getWallets() + const loader = waffle.createFixtureLoader([wallet, other, another, ...rest], provider) + const returns = { provider, wallet, other, another } + return { loader, returns } + } + + async function loadFixture(fixture: Fixture): Promise { + // This function creates a new provider for each fixture, because of bugs + // in ganache that clear contract code on evm_revert + const { loader, returns } = currentLoader.fixture === fixture ? currentLoader : await makeLoader() + currentLoader = { fixture, loader, returns } + const result = await loader(fixture) + return { ...returns, ...result } + } + + return loadFixture +} diff --git a/packages/contracts-tron/test/utils/hardhatPatches.ts b/packages/contracts-tron/test/utils/hardhatPatches.ts new file mode 100644 index 000000000..b91c43310 --- /dev/null +++ b/packages/contracts-tron/test/utils/hardhatPatches.ts @@ -0,0 +1,82 @@ +import { waffle } from 'hardhat' +import type { RecordedCall } from 'ethereum-waffle' +import { utils } from 'ethers' + +const init = (waffle.provider as any)._hardhatNetwork.provider._wrapped._wrapped._wrapped._init + +function patchSkipGasCostCheck() { + const originalProcess = (waffle.provider as any)._hardhatNetwork.provider._wrapped._wrapped._wrapped._ethModule.processRequest.bind( + (waffle.provider as any)._hardhatNetwork.provider._wrapped._wrapped._wrapped._ethModule, + ) + ;(waffle.provider as any)._hardhatNetwork.provider._wrapped._wrapped._wrapped._ethModule.processRequest = ( + method: string, + params: any[], + ) => { + if (method === 'eth_estimateGas') { + return '0xB71B00' + } else { + return originalProcess(method, params) + } + } +} + +class CallHistory { + recordedCalls: RecordedCall[] = []; + + addUniqueCall(call: RecordedCall) { + if (!this.recordedCalls.find(c => c.address === call.address && c.data === call.data)) { + this.recordedCalls.push(call) + } + } + + clearAll() { + this.recordedCalls = [] + } +} + +function toRecordedCall(message: any): RecordedCall { + return { + address: message.to?.buf ? utils.getAddress(utils.hexlify(message.to.buf)) : undefined, + data: message.data ? utils.hexlify(message.data) : '0x', + } +} + +const callHistory = new CallHistory(); +(waffle.provider as any).clearCallHistory = () => { + callHistory.clearAll() +} + +let beforeMessageListener: (message: any) => void | undefined; + +(waffle.provider as any)._hardhatNetwork.provider._wrapped._wrapped._wrapped._init = async function () { + await init.apply(this) + if (typeof beforeMessageListener === 'function') { + // hast to be here because of weird behaviour of init function + (waffle.provider as any) + ._hardhatNetwork + .provider + ._wrapped + ._wrapped + ._wrapped + ._node + ._vmTracer + ._vm + .off('beforeMessage', beforeMessageListener) + } + if ((waffle.provider as any)._hardhatNetwork.provider._wrapped._wrapped._wrapped._node._vmTracer._vm.listenerCount('beforeMessage') < 2) { + patchSkipGasCostCheck() + } + beforeMessageListener = (message: any) => { + callHistory.addUniqueCall(toRecordedCall(message)) + } + const provider: any = waffle.provider + provider.callHistory = callHistory.recordedCalls; + (waffle.provider as any) + ._hardhatNetwork.provider + ._wrapped._wrapped + ._wrapped + ._node + ._vmTracer + ._vm + .on('beforeMessage', beforeMessageListener) +} diff --git a/packages/contracts-tron/test/utils/timeTravel.ts b/packages/contracts-tron/test/utils/timeTravel.ts new file mode 100644 index 000000000..cb37f7f04 --- /dev/null +++ b/packages/contracts-tron/test/utils/timeTravel.ts @@ -0,0 +1,6 @@ +import { providers } from 'ethers' + +export const timeTravel = async (provider: providers.JsonRpcProvider, time: number) => { + await provider.send('evm_increaseTime', [time]) + await provider.send('evm_mine', []) +} diff --git a/packages/contracts-tron/tsconfig.json b/packages/contracts-tron/tsconfig.json new file mode 100644 index 000000000..3bcd8612f --- /dev/null +++ b/packages/contracts-tron/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "resolveJsonModule": true, + "esModuleInterop": true, + "moduleResolution": "node", + "skipLibCheck": true, + "target": "esnext", + "sourceMap": true, + "baseUrl": ".", + "paths": { + "build/*": ["build/*"], + "build": ["build"], + "fixtures/*": ["test/fixtures/*"], + "fixtures": ["test/fixtures"], + "contracts/*": ["build/types/*"], + "contracts": ["build/types", "build/types/factories"], + "utils/*": ["test/utils/*"], + "utils": ["test/utils"], + "config/*": ["test/config/*"], + "config": ["test/config"], + } + }, + "include": [ + "build", + "contracts", + "test" + ], + "files": ["./hardhat.config.ts"] +}