From 4f6c971f0ff148cda840f26b382653341c5f8c42 Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Fri, 11 Aug 2023 16:08:26 +0200 Subject: [PATCH 1/9] Switch to DIA price oracle (#86) DIA Oracle added to the InternalMarket close #85 --- .gitignore | 3 +- .openzeppelin/unknown-9001.json | 452 ++++++++++++++++++ contracts/InternalMarket/IDIAOracleV2.sol | 18 + contracts/InternalMarket/InternalMarket.sol | 5 +- .../InternalMarket/InternalMarketBase.sol | 15 +- contracts/PriceOracle/IStdReference.sol | 23 - contracts/PriceOracle/PriceOracle.sol | 72 --- contracts/mocks/DIAOracleV2Mock.sol | 49 ++ lib/config.ts | 7 +- lib/environment/memory.ts | 8 +- lib/internal/types.ts | 10 +- lib/sequence/deploy.ts | 14 +- lib/sequence/setup.ts | 11 + lib/utils.ts | 4 +- tasks/admin.ts | 15 + tasks/index.ts | 1 - tasks/oracle.ts | 59 --- test/Integration.ts | 1 - test/InternalMarket.ts | 145 +++--- test/PriceOracle.ts | 123 ----- 20 files changed, 640 insertions(+), 395 deletions(-) create mode 100644 contracts/InternalMarket/IDIAOracleV2.sol delete mode 100644 contracts/PriceOracle/IStdReference.sol delete mode 100644 contracts/PriceOracle/PriceOracle.sol create mode 100644 contracts/mocks/DIAOracleV2Mock.sol delete mode 100644 tasks/oracle.ts delete mode 100644 test/PriceOracle.ts diff --git a/.gitignore b/.gitignore index cc46584..9209bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ diagrams/* echidna/results/* .DS_Store echidna/results -diagrams/ \ No newline at end of file +diagrams/ +.openzeppelin/unknown-666666.json diff --git a/.openzeppelin/unknown-9001.json b/.openzeppelin/unknown-9001.json index 9a30f64..5344f1a 100644 --- a/.openzeppelin/unknown-9001.json +++ b/.openzeppelin/unknown-9001.json @@ -2934,6 +2934,458 @@ } } } + }, + "0b3621572808727f4a9449fa76563140af8313ccb9b2a1b871e2bf39371370ec": { + "address": "0x21b4A54A0B457f1C488CBB989bD221C283A42c19", + "txHash": "0x1f8596c52bb6d5ea3345bcb70de05a2624a3bebe5cbf231256477519fbffe885", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_roles", + "offset": 0, + "slot": "51", + "type": "t_contract(DAORoles)13019", + "contract": "HasRole", + "src": "contracts/extensions/HasRole.sol:11" + }, + { + "label": "tokenInternal", + "offset": 0, + "slot": "52", + "type": "t_contract(IGovernanceToken)7303", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:33" + }, + { + "label": "exchangeToken", + "offset": 0, + "slot": "53", + "type": "t_contract(ERC20)4378", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:36" + }, + { + "label": "redemptionController", + "offset": 0, + "slot": "54", + "type": "t_contract(IRedemptionController)8525", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:38" + }, + { + "label": "priceOracle", + "offset": 0, + "slot": "55", + "type": "t_contract(IDIAOracleV2)7329", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:39" + }, + { + "label": "_shareholderRegistry", + "offset": 0, + "slot": "56", + "type": "t_contract(IShareholderRegistry)10906", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:40" + }, + { + "label": "reserve", + "offset": 0, + "slot": "57", + "type": "t_address", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:42" + }, + { + "label": "offerDuration", + "offset": 0, + "slot": "58", + "type": "t_uint256", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:43" + }, + { + "label": "_offers", + "offset": 0, + "slot": "59", + "type": "t_mapping(t_address,t_struct(Offers)7667_storage)", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:45" + }, + { + "label": "_vaultContributors", + "offset": 0, + "slot": "60", + "type": "t_mapping(t_address,t_uint256)", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:47" + }, + { + "label": "_diaPriceOracle", + "offset": 0, + "slot": "61", + "type": "t_contract(IDIAOracleV2)7329", + "contract": "InternalMarket", + "src": "contracts/InternalMarket/InternalMarket.sol:21" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(DAORoles)13019": { + "label": "contract DAORoles", + "numberOfBytes": "20" + }, + "t_contract(ERC20)4378": { + "label": "contract ERC20", + "numberOfBytes": "20" + }, + "t_contract(IDIAOracleV2)7329": { + "label": "contract IDIAOracleV2", + "numberOfBytes": "20" + }, + "t_contract(IGovernanceToken)7303": { + "label": "contract IGovernanceToken", + "numberOfBytes": "20" + }, + "t_contract(IRedemptionController)8525": { + "label": "contract IRedemptionController", + "numberOfBytes": "20" + }, + "t_contract(IShareholderRegistry)10906": { + "label": "contract IShareholderRegistry", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(Offers)7667_storage)": { + "label": "mapping(address => struct InternalMarketBase.Offers)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint128,t_struct(Offer)7657_storage)": { + "label": "mapping(uint128 => struct InternalMarketBase.Offer)", + "numberOfBytes": "32" + }, + "t_struct(Offer)7657_storage": { + "label": "struct InternalMarketBase.Offer", + "members": [ + { + "label": "expiredAt", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Offers)7667_storage": { + "label": "struct InternalMarketBase.Offers", + "members": [ + { + "label": "start", + "type": "t_uint128", + "offset": 0, + "slot": "0" + }, + { + "label": "end", + "type": "t_uint128", + "offset": 16, + "slot": "0" + }, + { + "label": "offer", + "type": "t_mapping(t_uint128,t_struct(Offer)7657_storage)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint128": { + "label": "uint128", + "numberOfBytes": "16" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "fc8fccb6beac0421dda67147bbd35e89507ac4d5ab04bfaa9db6a19473f05fd8": { + "address": "0xA64A0B84da74e559E4fD2d05aEAA6582121D7FD7", + "txHash": "0xb60b93105c9a75906b511bfcdd2c3fc8ee893cfc466dfbbc0783dc7cf05777a4", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_roles", + "offset": 0, + "slot": "51", + "type": "t_contract(DAORoles)13007", + "contract": "HasRole", + "src": "contracts/extensions/HasRole.sol:11" + }, + { + "label": "tokenInternal", + "offset": 0, + "slot": "52", + "type": "t_contract(IGovernanceToken)7303", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:33" + }, + { + "label": "exchangeToken", + "offset": 0, + "slot": "53", + "type": "t_contract(ERC20)4378", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:36" + }, + { + "label": "redemptionController", + "offset": 0, + "slot": "54", + "type": "t_contract(IRedemptionController)8513", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:38" + }, + { + "label": "priceOracle", + "offset": 0, + "slot": "55", + "type": "t_contract(IDIAOracleV2)7329", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:39" + }, + { + "label": "_shareholderRegistry", + "offset": 0, + "slot": "56", + "type": "t_contract(IShareholderRegistry)10894", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:40" + }, + { + "label": "reserve", + "offset": 0, + "slot": "57", + "type": "t_address", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:42" + }, + { + "label": "offerDuration", + "offset": 0, + "slot": "58", + "type": "t_uint256", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:43" + }, + { + "label": "_offers", + "offset": 0, + "slot": "59", + "type": "t_mapping(t_address,t_struct(Offers)7655_storage)", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:45" + }, + { + "label": "_vaultContributors", + "offset": 0, + "slot": "60", + "type": "t_mapping(t_address,t_uint256)", + "contract": "InternalMarketBase", + "src": "contracts/InternalMarket/InternalMarketBase.sol:47" + }, + { + "label": "_diaPriceOracle", + "offset": 0, + "slot": "61", + "type": "t_contract(IDIAOracleV2)7329", + "contract": "InternalMarket", + "src": "contracts/InternalMarket/InternalMarket.sol:21" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(DAORoles)13007": { + "label": "contract DAORoles", + "numberOfBytes": "20" + }, + "t_contract(ERC20)4378": { + "label": "contract ERC20", + "numberOfBytes": "20" + }, + "t_contract(IDIAOracleV2)7329": { + "label": "contract IDIAOracleV2", + "numberOfBytes": "20" + }, + "t_contract(IGovernanceToken)7303": { + "label": "contract IGovernanceToken", + "numberOfBytes": "20" + }, + "t_contract(IRedemptionController)8513": { + "label": "contract IRedemptionController", + "numberOfBytes": "20" + }, + "t_contract(IShareholderRegistry)10894": { + "label": "contract IShareholderRegistry", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(Offers)7655_storage)": { + "label": "mapping(address => struct InternalMarketBase.Offers)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint128,t_struct(Offer)7645_storage)": { + "label": "mapping(uint128 => struct InternalMarketBase.Offer)", + "numberOfBytes": "32" + }, + "t_struct(Offer)7645_storage": { + "label": "struct InternalMarketBase.Offer", + "members": [ + { + "label": "expiredAt", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Offers)7655_storage": { + "label": "struct InternalMarketBase.Offers", + "members": [ + { + "label": "start", + "type": "t_uint128", + "offset": 0, + "slot": "0" + }, + { + "label": "end", + "type": "t_uint128", + "offset": 16, + "slot": "0" + }, + { + "label": "offer", + "type": "t_mapping(t_uint128,t_struct(Offer)7645_storage)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint128": { + "label": "uint128", + "numberOfBytes": "16" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/contracts/InternalMarket/IDIAOracleV2.sol b/contracts/InternalMarket/IDIAOracleV2.sol new file mode 100644 index 0000000..acbfcac --- /dev/null +++ b/contracts/InternalMarket/IDIAOracleV2.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +interface IDIAOracleV2 { + function setValue( + string memory key, + uint128 value, + uint128 timestamp + ) external; + + function getValue( + string memory key + ) external view returns (uint128 value, uint128 timestamp); + + function updateOracleUpdaterAddress( + address newOracleUpdaterAddress + ) external; +} diff --git a/contracts/InternalMarket/InternalMarket.sol b/contracts/InternalMarket/InternalMarket.sol index 7121cc5..29f472a 100644 --- a/contracts/InternalMarket/InternalMarket.sol +++ b/contracts/InternalMarket/InternalMarket.sol @@ -10,6 +10,7 @@ import "./InternalMarketBase.sol"; import { Roles } from "../extensions/Roles.sol"; import "../extensions/DAORoles.sol"; import "../extensions/HasRole.sol"; +import "./IDIAOracleV2.sol"; /** * @title InternalMarket @@ -17,6 +18,8 @@ import "../extensions/HasRole.sol"; * allowing them to make an offer, match existing offers, deposit, withdraw, and redeem locked tokens. */ contract InternalMarket is Initializable, HasRole, InternalMarketBase { + IDIAOracleV2 internal _diaPriceOracle; + /** * @dev Initializes the contract with the given roles and internal token. * @param roles DAORoles instance containing custom access control roles. @@ -118,7 +121,7 @@ contract InternalMarket is Initializable, HasRole, InternalMarketBase { */ function setExchangePair( ERC20 token, - IStdReference oracle + IDIAOracleV2 oracle ) public onlyRole(Roles.RESOLUTION_ROLE) diff --git a/contracts/InternalMarket/InternalMarketBase.sol b/contracts/InternalMarket/InternalMarketBase.sol index 225bcbb..833639d 100644 --- a/contracts/InternalMarket/InternalMarketBase.sol +++ b/contracts/InternalMarket/InternalMarketBase.sol @@ -5,8 +5,7 @@ pragma solidity ^0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../ShareholderRegistry/IShareholderRegistry.sol"; import "../RedemptionController/IRedemptionController.sol"; -import "../PriceOracle/IStdReference.sol"; - +import "./IDIAOracleV2.sol"; import "../NeokingdomToken/INeokingdomToken.sol"; import "../GovernanceToken/IGovernanceToken.sol"; @@ -37,7 +36,7 @@ contract InternalMarketBase { ERC20 public exchangeToken; IRedemptionController public redemptionController; - IStdReference public priceOracle; + IDIAOracleV2 public priceOracle; IShareholderRegistry internal _shareholderRegistry; address public reserve; @@ -75,7 +74,7 @@ contract InternalMarketBase { function _setExchangePair( ERC20 token, - IStdReference oracle + IDIAOracleV2 oracle ) internal virtual { exchangeToken = token; priceOracle = oracle; @@ -248,9 +247,11 @@ contract InternalMarketBase { redemptionController.afterRedeem(from, amount); } - function _convertToUSDC(uint256 eurAmount) internal view returns (uint256) { - uint256 eurUsd = priceOracle.getReferenceData("EUR", "USD").rate; - uint256 usdUsdc = priceOracle.getReferenceData("USDC", "USD").rate; + function _convertToUSDC( + uint256 eurAmount + ) internal view virtual returns (uint256) { + (uint256 eurUsd, ) = priceOracle.getValue("EUR/USD"); + (uint256 usdUsdc, ) = priceOracle.getValue("USDC/USD"); // 18 is the default amount of decimals for ERC20 tokens, including neokingdom ones return diff --git a/contracts/PriceOracle/IStdReference.sol b/contracts/PriceOracle/IStdReference.sol deleted file mode 100644 index aad6a6c..0000000 --- a/contracts/PriceOracle/IStdReference.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; - -interface IStdReference { - /// A structure returned whenever someone requests for standard reference data. - struct ReferenceData { - uint256 rate; // base/quote exchange rate, multiplied by 1e18. - uint256 lastUpdatedBase; // UNIX epoch of the last time when base price gets updated. - uint256 lastUpdatedQuote; // UNIX epoch of the last time when quote price gets updated. - } - - /// Returns the price data for the given base/quote pair. Revert if not available. - function getReferenceData( - string memory _base, - string memory _quote - ) external view returns (ReferenceData memory); - - /// Similar to getReferenceData, but with multiple base/quote pairs at once. - function getReferenceDataBulk( - string[] memory _bases, - string[] memory _quotes - ) external view returns (ReferenceData[] memory); -} diff --git a/contracts/PriceOracle/PriceOracle.sol b/contracts/PriceOracle/PriceOracle.sol deleted file mode 100644 index 3d45253..0000000 --- a/contracts/PriceOracle/PriceOracle.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; - -import "./IStdReference.sol"; -import "@openzeppelin/contracts/access/AccessControl.sol"; - -contract PriceOracle is IStdReference, AccessControl { - event RefDataUpdate(string symbol, uint64 rate, uint64 resolveTime); - - struct RefData { - uint64 rate; // USD-rate, multiplied by 1e18. - uint64 resolveTime; // UNIX epoch when data is last resolved. - } - - mapping(string => RefData) public refs; // Mapping from symbol to ref data. - bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE"); - - constructor() { - _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); - // TODO: leave this out - _setupRole(RELAYER_ROLE, msg.sender); - } - - function relay( - string[] memory _symbols, - uint64[] memory _rates, - uint64[] memory _resolveTimes - ) external onlyRole(RELAYER_ROLE) { - uint256 len = _symbols.length; - require(_rates.length == len, "BAD_RATES_LENGTH"); - require(_resolveTimes.length == len, "BAD_RESOLVE_TIMES_LENGTH"); - for (uint256 idx = 0; idx < len; idx++) { - refs[_symbols[idx]] = RefData({ - rate: _rates[idx], - resolveTime: _resolveTimes[idx] - }); - emit RefDataUpdate(_symbols[idx], _rates[idx], _resolveTimes[idx]); - } - } - - function getReferenceData( - string memory _base, - string memory _quote - ) public view override returns (ReferenceData memory) { - (uint256 baseRate, uint256 baseLastUpdate) = _getRefData(_base); - (uint256 quoteRate, uint256 quoteLastUpdate) = _getRefData(_quote); - return - ReferenceData({ - rate: (baseRate * 1e18) / quoteRate, - lastUpdatedBase: baseLastUpdate, - lastUpdatedQuote: quoteLastUpdate - }); - } - - function getReferenceDataBulk( - string[] memory, - string[] memory - ) public pure override returns (ReferenceData[] memory) { - revert("NOT_IMPLEMENTED"); - } - - function _getRefData( - string memory _symbol - ) internal view returns (uint256 rate, uint256 lastUpdate) { - if (keccak256(bytes(_symbol)) == keccak256(bytes("USD"))) { - return (1e18, block.timestamp); - } - RefData storage refData = refs[_symbol]; - require(refData.resolveTime > 0, "REF_DATA_NOT_AVAILABLE"); - return (uint256(refData.rate), uint256(refData.resolveTime)); - } -} diff --git a/contracts/mocks/DIAOracleV2Mock.sol b/contracts/mocks/DIAOracleV2Mock.sol new file mode 100644 index 0000000..81c4a76 --- /dev/null +++ b/contracts/mocks/DIAOracleV2Mock.sol @@ -0,0 +1,49 @@ +/** + *Submitted for verification at escan.live on 2023-06-20 + */ + +// compiled using solidity 0.7.4 + +pragma solidity ^0.8.16; + +contract DIAOracleV2Mock { + mapping(string => uint256) public values; + address oracleUpdater; + + event OracleUpdate(string key, uint128 value, uint128 timestamp); + event UpdaterAddressChange(address newUpdater); + + constructor() { + oracleUpdater = msg.sender; + setValue("USDC/USD", 100056862, 1688997110); + setValue("EUR/USD", 109479913, 1688997110); + } + + function setValue( + string memory key, + uint128 value, + uint128 timestamp + ) public { + require(msg.sender == oracleUpdater); + uint256 cValue = (((uint256)(value)) << 128) + timestamp; + values[key] = cValue; + emit OracleUpdate(key, value, timestamp); + } + + function getValue( + string memory key + ) external view returns (uint128, uint128) { + uint256 cValue = values[key]; + uint128 timestamp = (uint128)(cValue % 2 ** 128); + uint128 value = (uint128)(cValue >> 128); + return (value, timestamp); + } + + function updateOracleUpdaterAddress( + address newOracleUpdaterAddress + ) public { + require(msg.sender == oracleUpdater); + oracleUpdater = newOracleUpdaterAddress; + emit UpdaterAddressChange(newOracleUpdaterAddress); + } +} diff --git a/lib/config.ts b/lib/config.ts index e63a11e..b97767c 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -3,8 +3,8 @@ import { readFile } from "fs/promises"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { + DIAOracleV2Mock__factory, GovernanceToken__factory, - PriceOracle__factory, ProxyAdmin__factory, ResolutionManager__factory, ShareholderRegistry__factory, @@ -54,8 +54,8 @@ type ContractFactory = | typeof GovernanceToken__factory | typeof Voting__factory | typeof TokenMock__factory - | typeof PriceOracle__factory - | typeof ProxyAdmin__factory; + | typeof ProxyAdmin__factory + | typeof DIAOracleV2Mock__factory; export async function loadContract( hre: HardhatRuntimeEnvironment, @@ -70,6 +70,7 @@ export async function loadContract( const [deployer] = await hre.ethers.getSigners(); const { chainId, name: networkName } = await hre.ethers.provider.getNetwork(); const addresses = networks[chainId]; + console.log(networks); if (!addresses || !addresses[name]) { console.error(`Cannot find address for ${name} in network ${networkName}.`); diff --git a/lib/environment/memory.ts b/lib/environment/memory.ts index ddff8fc..8374526 100644 --- a/lib/environment/memory.ts +++ b/lib/environment/memory.ts @@ -3,10 +3,10 @@ import { ethers, upgrades } from "hardhat"; import { DAORoles, + DIAOracleV2Mock, GovernanceToken, InternalMarket, NeokingdomToken, - PriceOracle, ProxyAdmin, RedemptionController, ResolutionManager, @@ -79,9 +79,6 @@ export class NeokingdomDAOMemory extends NeokingdomDAO { case "NeokingdomToken": this.contracts.neokingdomToken = contract as NeokingdomToken; break; - case "PriceOracle": - this.contracts.priceOracle = contract as PriceOracle; - break; case "RedemptionController": this.contracts.redemptionController = contract as RedemptionController; break; @@ -99,6 +96,9 @@ export class NeokingdomDAOMemory extends NeokingdomDAO { break; case "ProxyAdmin": this.contracts.proxyAdmin = contract as ProxyAdmin; + break; + case "DIAOracleV2Mock": + this.contracts.diaOracleV2Mock = contract as DIAOracleV2Mock; } } } diff --git a/lib/internal/types.ts b/lib/internal/types.ts index d27e2ba..7aec239 100644 --- a/lib/internal/types.ts +++ b/lib/internal/types.ts @@ -6,14 +6,14 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DAORoles, DAORoles__factory, + DIAOracleV2Mock, + DIAOracleV2Mock__factory, GovernanceToken, GovernanceToken__factory, InternalMarket, InternalMarket__factory, NeokingdomToken, NeokingdomToken__factory, - PriceOracle, - PriceOracle__factory, ProxyAdmin, ProxyAdmin__factory, RedemptionController, @@ -34,13 +34,13 @@ export const FACTORIES = { InternalMarket: InternalMarket__factory, GovernanceToken: GovernanceToken__factory, NeokingdomToken: NeokingdomToken__factory, - PriceOracle: PriceOracle__factory, RedemptionController: RedemptionController__factory, ResolutionManager: ResolutionManager__factory, ShareholderRegistry: ShareholderRegistry__factory, TokenMock: TokenMock__factory, Voting: Voting__factory, ProxyAdmin: ProxyAdmin__factory, + DIAOracleV2Mock: DIAOracleV2Mock__factory, } as const; export type ContractNames = keyof typeof FACTORIES; @@ -61,13 +61,13 @@ export type NeokingdomContracts = { internalMarket: InternalMarket; governanceToken: GovernanceToken; neokingdomToken: NeokingdomToken; - priceOracle: PriceOracle; redemptionController: RedemptionController; resolutionManager: ResolutionManager; shareholderRegistry: ShareholderRegistry; tokenMock: TokenMock; voting: Voting; proxyAdmin: ProxyAdmin; + diaOracleV2Mock: DIAOracleV2Mock; }; export type Context = {}; @@ -94,13 +94,13 @@ export const CONTRACT_NAMES = [ "internalMarket", "governanceToken", "neokingdomToken", - "priceOracle", "redemptionController", "resolutionManager", "shareholderRegistry", "tokenMock", "voting", "proxyAdmin", + "diaOracleV2Mock", ]; export function isNeokingdomContracts( diff --git a/lib/sequence/deploy.ts b/lib/sequence/deploy.ts index 9af4135..26de7f2 100644 --- a/lib/sequence/deploy.ts +++ b/lib/sequence/deploy.ts @@ -42,7 +42,7 @@ export const DEPLOY_SEQUENCE: Sequence = [ ///////////////////// (c) => c.deploy("DAORoles"), (c) => c.deploy("TokenMock"), - (c) => c.deploy("PriceOracle"), + (c) => c.deploy("DIAOracleV2Mock"), (c) => c.deployProxy("Voting", [c.daoRoles.address]), (c) => c.deployProxy("GovernanceToken", [ @@ -71,8 +71,6 @@ export const DEPLOY_SEQUENCE: Sequence = [ c.voting.address, ]), (c) => c.deploy("ProxyAdmin"), - (c) => c.priceOracle.relay(["EUR", "USD"], [1, 1], [1, 1]), - (c) => c.priceOracle.relay(["USDC", "USD"], [1, 1], [1, 1]), // Set ACLs ///////////// @@ -123,13 +121,13 @@ export const DEPLOY_SEQUENCE: Sequence = [ (c) => c.internalMarket.setRedemptionController(c.redemptionController.address), + (c) => c.internalMarket.setReserve(c.reserve), + (c) => c.internalMarket.setShareholderRegistry(c.shareholderRegistry.address), + (c) => c.diaOracleV2Mock.setValue("EUR/USD", 100000000, 1688997107), + (c) => c.diaOracleV2Mock.setValue("USDC/USD", 100000000, 1688997107), (c) => c.internalMarket.setExchangePair( c.tokenMock.address, - c.priceOracle.address - //"0x15c3eb3b621d1bff62cba1c9536b7c1ae9149b57", - //"0x666CDb721838B1b8C0C234DAa0D9Dbc821103aA5" + c.diaOracleV2Mock.address ), - (c) => c.internalMarket.setReserve(c.reserve), - (c) => c.internalMarket.setShareholderRegistry(c.shareholderRegistry.address), ]; diff --git a/lib/sequence/setup.ts b/lib/sequence/setup.ts index 1fe669b..8ce4899 100644 --- a/lib/sequence/setup.ts +++ b/lib/sequence/setup.ts @@ -5,6 +5,12 @@ import { expandable } from "../internal/core"; import { Sequence, SetupContext } from "../internal/types"; export const SETUP_SEQUENCE: Sequence = [ + (c) => + c.internalMarket.setExchangePair( + "0x15c3eb3b621d1bff62cba1c9536b7c1ae9149b57", // USDC + "0x3141274e597116f0bfcf07aeafa81b6b39c94325" // DIA Price Oracle + ), + // Give each address one share expandable((preprocessContext: SetupContext) => preprocessContext.contributors.map( @@ -61,6 +67,11 @@ export const SETUP_SEQUENCE_TESTNET: Sequence = [ 60 * 3, false ), + (c) => + c.internalMarket.setExchangePair( + c.tokenMock.address, + c.diaOracleV2Mock.address + ), expandable((preprocessContext: SetupContext) => preprocessContext.contributors.map( (contributor) => (c) => diff --git a/lib/utils.ts b/lib/utils.ts index bd925d8..f9dd898 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -7,10 +7,10 @@ import * as readline from "readline"; import { DAORoles, + DIAOracleV2Mock, GovernanceToken, InternalMarket, NeokingdomToken, - PriceOracle, ProxyAdmin, RedemptionController, ResolutionManager, @@ -204,7 +204,6 @@ export async function loadContracts( internalMarket: await _loadContract("InternalMarket"), governanceToken: await _loadContract("GovernanceToken"), neokingdomToken: await _loadContract("NeokingdomToken"), - priceOracle: await _loadContract("PriceOracle"), redemptionController: await _loadContract( "RedemptionController" ), @@ -217,6 +216,7 @@ export async function loadContracts( tokenMock: await _loadContract("TokenMock"), voting: await _loadContract("Voting"), proxyAdmin: await _loadContract("ProxyAdmin"), + diaOracleV2Mock: await _loadContract("DIAOracleV2Mock"), }; } diff --git a/tasks/admin.ts b/tasks/admin.ts index e2019b6..084359a 100644 --- a/tasks/admin.ts +++ b/tasks/admin.ts @@ -13,3 +13,18 @@ task("admin:transfer", "Transfer ProxyAdmin ownership") console.log("Done"); }); + +task("admin:exchange:set", "Set market exchange pair") + .addParam("oracle", "Oracle Address") + .addParam("usdc", "USDC Address") + .setAction( + async ({ oracle, usdc }: { oracle: string; usdc: string }, hre) => { + const neokingdom = await NeokingdomDAOHardhat.initialize(hre); + const contracts = await neokingdom.loadContracts(); + + const tx = await contracts.internalMarket.setExchangePair(usdc, oracle); + await tx.wait(1); + + console.log("Done"); + } + ); diff --git a/tasks/index.ts b/tasks/index.ts index 12b83e5..38adcaa 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -1,7 +1,6 @@ import "./admin"; import "./delegate"; import "./deploy"; -import "./oracle"; import "./resolution"; import "./status"; import "./tokens"; diff --git a/tasks/oracle.ts b/tasks/oracle.ts deleted file mode 100644 index cf5ee37..0000000 --- a/tasks/oracle.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { task } from "hardhat/config"; - -import { PriceOracle__factory } from "../typechain"; - -import { loadContract } from "../lib/config"; - -task("ref", "Get reference data") - .addPositionalParam("base", "Base symbol") - .addPositionalParam("quote", "Quote symbol") - .setAction(async ({ base, quote }: { base: string; quote: string }, hre) => { - const contract = await loadContract( - hre, - PriceOracle__factory, - "PriceOracle" - ); - - const result = await contract.getReferenceData(base, quote); - console.log(result); - }); - -task("add-relayer", "Add relayer") - .addPositionalParam("account", "Relayer address") - .setAction(async ({ account }: { account: string }, hre) => { - const contract = await loadContract( - hre, - PriceOracle__factory, - "PriceOracle" - ); - - const tx = await contract.grantRole(await contract.RELAYER_ROLE(), account); - await tx.wait(1); - console.log("Done"); - }); - -task("relay", "Add reference data") - .addParam("symbol", "Symbol") - .addParam("rate", "Rate") - .addParam("time", "Resolve time") - .setAction( - async ( - { symbol, rate, time }: { symbol: string; rate: number; time: number }, - hre - ) => { - const contract = await loadContract( - hre, - PriceOracle__factory, - "PriceOracle" - ); - - console.log("Relyaing"); - console.log(` Symbols ${symbol}`); - console.log(` Rates ${rate}`); - console.log(` Times ${time}`); - - const tx = await contract.relay([symbol], [rate], [time]); - await tx.wait(1); - console.log("Done"); - } - ); diff --git a/test/Integration.ts b/test/Integration.ts index 7c0475c..40e354c 100644 --- a/test/Integration.ts +++ b/test/Integration.ts @@ -93,7 +93,6 @@ describe("Integration", async () => { governanceToken, neokingdomToken, shareholderRegistry, - resolutionManager, internalMarket, redemptionController, diff --git a/test/InternalMarket.ts b/test/InternalMarket.ts index c85ac38..a22626e 100644 --- a/test/InternalMarket.ts +++ b/test/InternalMarket.ts @@ -12,11 +12,11 @@ import { ERC20, IGovernanceToken, IRedemptionController, - IStdReference, InternalMarket, InternalMarket__factory, ShareholderRegistry, } from "../typechain"; +import { IDIAOracleV2 } from "../typechain/contracts/InternalMarket/IDIAOracleV2"; import { getEVMTimestamp, mineEVMBlock, setEVMTimestamp } from "./utils/evm"; import { roles } from "./utils/roles"; @@ -38,7 +38,7 @@ describe("InternalMarket", async () => { let registry: FakeContract; let internalMarket: InternalMarket; let redemption: FakeContract; - let stdReference: FakeContract; + let oracle: FakeContract; let usdc: FakeContract; let deployer: SignerWithAddress; let alice: SignerWithAddress; @@ -69,7 +69,7 @@ describe("InternalMarket", async () => { ])) as InternalMarket; redemption = await smock.fake("IRedemptionController"); - stdReference = await smock.fake("IStdReference"); + oracle = await smock.fake("IDIAOracleV2"); registry = await smock.fake("ShareholderRegistry"); RESOLUTION_ROLE = await roles.RESOLUTION_ROLE(); @@ -78,7 +78,7 @@ describe("InternalMarket", async () => { .whenCalledWith(RESOLUTION_ROLE, deployer.address) .returns(true); await internalMarket.setRedemptionController(redemption.address); - await internalMarket.setExchangePair(usdc.address, stdReference.address); + await internalMarket.setExchangePair(usdc.address, oracle.address); await internalMarket.setReserve(reserve.address); await internalMarket.setShareholderRegistry(registry.address); @@ -92,17 +92,16 @@ describe("InternalMarket", async () => { governanceToken.unwrap.reset(); usdc.transfer.reset(); usdc.transferFrom.reset(); - stdReference.getReferenceData.reset(); + oracle.getValue.reset(); registry.isAtLeast.returns(true); // make transferFrom always succeed governanceToken.transferFrom.returns(true); // Exchange rate is always 1 - stdReference.getReferenceData.returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.returns({ + value: parseEther("1"), + timestamp: parseEther("0"), }); }); @@ -220,18 +219,14 @@ describe("InternalMarket", async () => { describe("when the exchange rate is 1/1", async () => { beforeEach(async () => { - stdReference.getReferenceData.whenCalledWith("EUR", "USD").returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("EUR/USD").returns({ + value: parseEther("1"), + timestamp: parseEther("0"), + }); + oracle.getValue.whenCalledWith("USDC/USD").returns({ + value: parseEther("1"), + timestamp: parseEther("0"), }); - stdReference.getReferenceData - .whenCalledWith("USDC", "USD") - .returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), - }); }); it("should burn the 10 DAO tokens for 10 USDC of the reserve", async () => { @@ -250,18 +245,14 @@ describe("InternalMarket", async () => { describe("when the exchange rate is 1/2", async () => { beforeEach(async () => { - stdReference.getReferenceData.whenCalledWith("EUR", "USD").returns({ - rate: parseEther("2"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("EUR/USD").returns({ + value: parseEther("2"), + timestamp: parseEther("0"), + }); + oracle.getValue.whenCalledWith("USDC/USD").returns({ + value: parseEther("1"), + timestamp: parseEther("0"), }); - stdReference.getReferenceData - .whenCalledWith("USDC", "USD") - .returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), - }); }); it("should burn 10 DAO token for 20 USDC", async () => { @@ -281,18 +272,14 @@ describe("InternalMarket", async () => { describe("when the exchange rate is 1.12 eur/usd and 0.998 usdc/usd", async () => { beforeEach(async () => { - stdReference.getReferenceData.whenCalledWith("EUR", "USD").returns({ - rate: parseEther("1.12"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("EUR/USD").returns({ + value: parseEther("1.12"), + timestamp: parseEther("0"), + }); + oracle.getValue.whenCalledWith("USDC/USD").returns({ + value: parseEther("0.998"), + timestamp: parseEther("0"), }); - stdReference.getReferenceData - .whenCalledWith("USDC", "USD") - .returns({ - rate: parseEther("0.998"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), - }); }); it("should burn the 10 DAO tokens for 11.222444 USDC", async () => { @@ -311,18 +298,14 @@ describe("InternalMarket", async () => { describe("when the exchange rate is 2/1", async () => { beforeEach(async () => { - stdReference.getReferenceData.whenCalledWith("EUR", "USD").returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("EUR/USD").returns({ + value: parseEther("1"), + timestamp: parseEther("0"), + }); + oracle.getValue.whenCalledWith("USDC/USD").returns({ + value: parseEther("2"), + timestamp: parseEther("0"), }); - stdReference.getReferenceData - .whenCalledWith("USDC", "USD") - .returns({ - rate: parseEther("2"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), - }); }); it("should burn the 11 DAO tokens for 5.5 USDC", async () => { @@ -575,15 +558,13 @@ describe("InternalMarket", async () => { describe("when the exchange rate is 1/1", async () => { beforeEach(async () => { - stdReference.getReferenceData.whenCalledWith("EUR", "USD").returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("EUR/USD").returns({ + value: parseEther("1"), + timestamp: parseEther("0"), }); - stdReference.getReferenceData.whenCalledWith("USDC", "USD").returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("USDC/USD").returns({ + value: parseEther("1"), + timestamp: parseEther("0"), }); }); @@ -607,15 +588,13 @@ describe("InternalMarket", async () => { describe("when the exchange rate is 1/2", async () => { beforeEach(async () => { - stdReference.getReferenceData.whenCalledWith("EUR", "USD").returns({ - rate: parseEther("2"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("EUR/USD").returns({ + value: parseEther("2"), + timestamp: parseEther("0"), }); - stdReference.getReferenceData.whenCalledWith("USDC", "USD").returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("USDC/USD").returns({ + value: parseEther("1"), + timestamp: parseEther("0"), }); }); @@ -639,15 +618,13 @@ describe("InternalMarket", async () => { describe("when the exchange rate is 1.12 eur/usd and 0.998 usdc/usd", async () => { beforeEach(async () => { - stdReference.getReferenceData.whenCalledWith("EUR", "USD").returns({ - rate: parseEther("1.12"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("EUR/USD").returns({ + value: parseEther("1.12"), + timestamp: parseEther("0"), }); - stdReference.getReferenceData.whenCalledWith("USDC", "USD").returns({ - rate: parseEther("0.998"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("USDC/USD").returns({ + value: parseEther("0.998"), + timestamp: parseEther("0"), }); }); @@ -671,15 +648,13 @@ describe("InternalMarket", async () => { describe("when the exchange rate is 2/1", async () => { beforeEach(async () => { - stdReference.getReferenceData.whenCalledWith("EUR", "USD").returns({ - rate: parseEther("1"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("EUR/USD").returns({ + value: parseEther("1"), + timestamp: parseEther("0"), }); - stdReference.getReferenceData.whenCalledWith("USDC", "USD").returns({ - rate: parseEther("2"), - lastUpdatedBase: parseEther("0"), - lastUpdatedQuote: parseEther("0"), + oracle.getValue.whenCalledWith("USDC/USD").returns({ + value: parseEther("2"), + timestamp: parseEther("0"), }); }); diff --git a/test/PriceOracle.ts b/test/PriceOracle.ts deleted file mode 100644 index 7bfd934..0000000 --- a/test/PriceOracle.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import chai from "chai"; -import chaiAsPromised from "chai-as-promised"; -import { solidity } from "ethereum-waffle"; -import { BigNumber } from "ethers"; -import { parseEther } from "ethers/lib/utils"; -import { ethers, network } from "hardhat"; - -import { PriceOracle, PriceOracle__factory } from "../typechain"; - -chai.use(solidity); -chai.use(chaiAsPromised); -const { expect } = chai; -let snapshotId: string; - -describe("PriceOracle", async () => { - let priceOracle: PriceOracle; - let deployer: SignerWithAddress, account: SignerWithAddress; - - before(async () => { - [deployer, account] = await ethers.getSigners(); - - const PriceOracleFactory = (await ethers.getContractFactory( - "PriceOracle", - deployer - )) as PriceOracle__factory; - - priceOracle = await PriceOracleFactory.deploy(); - await priceOracle.deployed(); - }); - - beforeEach(async () => { - snapshotId = await network.provider.send("evm_snapshot"); - }); - - afterEach(async () => { - await network.provider.send("evm_revert", [snapshotId]); - }); - - describe("relay", async () => { - it("should fail if not called by a relayer", async () => { - await expect( - priceOracle.connect(account).relay(["test"], [42], [43]) - ).revertedWith( - `AccessControl: account ${account.address.toLowerCase()} is missing role ${await priceOracle.RELAYER_ROLE()}` - ); - }); - - it("should fail if rates length doesn't match symbol length", async () => { - await expect(priceOracle.relay(["test"], [42, 43], [43])).revertedWith( - "BAD_RATES_LENGTH" - ); - }); - - it("should fail if resolve times length doesn't match symbol length", async () => { - await expect(priceOracle.relay(["test"], [42], [43, 42])).revertedWith( - "BAD_RESOLVE_TIMES_LENGTH" - ); - }); - - it("should emit 1 event per element", async () => { - await expect(priceOracle.relay(["test1", "test2"], [42, 43], [44, 45])) - .to.emit(priceOracle, "RefDataUpdate") - .withArgs("test1", 42, 44) - .to.emit(priceOracle, "RefDataUpdate") - .withArgs("test2", 43, 45); - }); - }); - - describe("getReferenceDataBulk", async () => { - it("should fail", async () => { - await expect(priceOracle.getReferenceDataBulk([], [])).revertedWith( - "NOT_IMPLEMENTED" - ); - }); - }); - - describe("getReferenceData", async () => { - it("should fail if called with non saved _base", async () => { - await priceOracle.relay(["EEUR"], [42], [43]); - await expect(priceOracle.getReferenceData("FAIL", "EEUR")).revertedWith( - "REF_DATA_NOT_AVAILABLE" - ); - }); - - it("should fail if called with non saved _quote", async () => { - await priceOracle.relay(["EEUR"], [42], [43]); - await expect(priceOracle.getReferenceData("EEUR", "FAIL")).revertedWith( - "REF_DATA_NOT_AVAILABLE" - ); - }); - - it("should return ratio in reference data", async () => { - const eeurUsd = parseEther("0.975286"); - const eurUsd = parseEther("1.032572"); - await priceOracle.relay(["EEUR", "EUR"], [eeurUsd, eurUsd], [43, 44]); - - const result = await priceOracle.getReferenceData("EEUR", "EUR"); - - expect(result[0]).equal(BigNumber.from("944521060032617580")); - expect(result[1]).equal(43); - expect(result[2]).equal(44); - }); - - it("should return same when _quote is USD", async () => { - const eeurUsd = parseEther("0.975286"); - await priceOracle.relay(["EEUR"], [eeurUsd], [43]); - - const result = await priceOracle.getReferenceData("EEUR", "USD"); - - expect(result[0]).equal(BigNumber.from("975286000000000000")); - }); - - it("should return 1 / _quote when _base is USD", async () => { - const eeurUsd = parseEther("0.975286"); - await priceOracle.relay(["EEUR"], [eeurUsd], [43]); - - const result = await priceOracle.getReferenceData("USD", "EEUR"); - - expect(result[0]).equal(BigNumber.from("1025340259165003906")); - }); - }); -}); From b204ae028ce5f87b04f88835ed320650bda562b5 Mon Sep 17 00:00:00 2001 From: vrde Date: Fri, 11 Aug 2023 16:14:16 +0200 Subject: [PATCH 2/9] Fix bug in `Voting.getTotalVotingPowerAt` for resolution with exclusion (#106) The [failing test](https://github.com/NeokingdomDAO/contracts/pull/106/commits/5ec10353bd58531f22dbb2e3b36d77664c338996) and [failing ci](https://github.com/NeokingdomDAO/contracts/actions/runs/5696209271/job/15440879869). The bug wasn't affecting any functionality. The fix simplifies the code, see #104 for an explanation. Close #104 --------- Co-authored-by: Nicola Miotto --- .../ResolutionManagerBase.sol | 50 ++++----- .../IShareholderRegistry.sol | 2 + contracts/mocks/ShareholderRegistryMock.sol | 3 + test/Integration.ts | 41 +++++++ test/ResolutionManager.ts | 104 +++--------------- 5 files changed, 85 insertions(+), 115 deletions(-) diff --git a/contracts/ResolutionManager/ResolutionManagerBase.sol b/contracts/ResolutionManager/ResolutionManagerBase.sol index 557a275..4d0e873 100644 --- a/contracts/ResolutionManager/ResolutionManagerBase.sol +++ b/contracts/ResolutionManager/ResolutionManagerBase.sol @@ -213,25 +213,31 @@ abstract contract ResolutionManagerBase { // power. Hence we are forcing the the contributor to have no delegation for this // resolution so to have the voting power "clean". Delegation is restored // after the snapshot. - address delegated; - if (resolution.addressedContributor != address(0)) { - delegated = _voting.getDelegate(resolution.addressedContributor); - if (delegated != resolution.addressedContributor) { - _voting.delegateFrom( - resolution.addressedContributor, - resolution.addressedContributor - ); - } + address addressedContributor = resolution.addressedContributor; + address originalDelegate; + bytes32 originalStatus; + + if (addressedContributor != address(0)) { + originalDelegate = _voting.getDelegate(addressedContributor); + originalStatus = _shareholderRegistry.getStatus( + addressedContributor + ); + // Downgrading to investor removes delegation + _shareholderRegistry.setStatus( + _shareholderRegistry.INVESTOR_STATUS(), + addressedContributor + ); } resolution.snapshotId = _snapshotAll(); - if (resolution.addressedContributor != address(0)) { - if (delegated != resolution.addressedContributor) { - _voting.delegateFrom( - resolution.addressedContributor, - delegated - ); + if (addressedContributor != address(0)) { + _shareholderRegistry.setStatus( + originalStatus, + addressedContributor + ); + if (addressedContributor != originalDelegate) { + _voting.delegateFrom(addressedContributor, originalDelegate); } } } @@ -363,7 +369,7 @@ abstract contract ResolutionManagerBase { resolution.snapshotId ); - // If sender has a delegate load voting power from GovernanceToken + // If sender has a delegate, load voting power from GovernanceToken if (delegate != msg.sender) { votingPower = _governanceToken.balanceOfAt( @@ -493,18 +499,6 @@ abstract contract ResolutionManagerBase { resolution.snapshotId ); - if (resolution.addressedContributor != address(0)) { - totalVotingPower -= - _governanceToken.balanceOfAt( - resolution.addressedContributor, - resolution.snapshotId - ) + - _shareholderRegistry.balanceOfAt( - resolution.addressedContributor, - resolution.snapshotId - ); - } - bool hasQuorum = resolution.yesVotesTotal * 100 >= resolutionType.quorum * totalVotingPower; diff --git a/contracts/ShareholderRegistry/IShareholderRegistry.sol b/contracts/ShareholderRegistry/IShareholderRegistry.sol index 6044fcc..7243337 100644 --- a/contracts/ShareholderRegistry/IShareholderRegistry.sol +++ b/contracts/ShareholderRegistry/IShareholderRegistry.sol @@ -13,6 +13,8 @@ interface IShareholderRegistry is ISnapshot { function MANAGING_BOARD_STATUS() external view returns (bytes32); + function setStatus(bytes32 status, address account) external; + function getStatus(address account) external view returns (bytes32); function getStatusAt( diff --git a/contracts/mocks/ShareholderRegistryMock.sol b/contracts/mocks/ShareholderRegistryMock.sol index 171a01b..17c1609 100644 --- a/contracts/mocks/ShareholderRegistryMock.sol +++ b/contracts/mocks/ShareholderRegistryMock.sol @@ -40,6 +40,9 @@ contract ShareholderRegistryMock is Initializable, IShareholderRegistry { return mockResult_isAtLeast[status][account]; } + // Unneeded for testing + function setStatus(bytes32 status, address account) public {} + // Unneeded for testing function getStatus( address account diff --git a/test/Integration.ts b/test/Integration.ts index 40e354c..48e6070 100644 --- a/test/Integration.ts +++ b/test/Integration.ts @@ -1459,6 +1459,47 @@ describe("Integration", async () => { }); }); + it("total voting power for a resolution with exclusion doesn't include the voting power of the excluded contributor", async () => { + await _makeContributor(user1, 20); + await _makeContributor(user2, 70); + await _makeContributor(user3, 10); + + // We have an extra of 4 shares: + // 3 are the shares of user1, user2, and user3 + // 1 is the share of the board + expect(await voting.getTotalVotingPower()).equal(e(20 + 70 + 10 + 4)); + + // resolution 1 excludes user 2 + const abi = ["function setStatus(bytes32 status, address account)"]; + const iface = new ethers.utils.Interface(abi); + const data = iface.encodeFunctionData("setStatus", [ + investorStatus, + user2.address, + ]); + + // Contributors propose a distrust vote against user3 + const distrust = ++currentResolution; + await resolutionManager + .connect(user1) + .createResolutionWithExclusion( + "Qxdistrust", + 0, + [shareholderRegistry.address], + [data], + user2.address + ); + + await resolutionManager + .connect(managingBoard) + .approveResolution(distrust); + const { snapshotId } = await resolutionManager.resolutions(distrust); + + // Now we have 3 extra shares as user2 is not a contributor anymore + expect(await voting.getTotalVotingPowerAt(snapshotId)).equal( + e(20 + 10 + 3) + ); + }); + it("voting with exclusion stress test", async () => { await _makeContributor(user1, 20); await _makeContributor(user2, 70); diff --git a/test/ResolutionManager.ts b/test/ResolutionManager.ts index 8e4b312..8ecf6ee 100644 --- a/test/ResolutionManager.ts +++ b/test/ResolutionManager.ts @@ -35,6 +35,8 @@ describe("Resolution", async () => { let resolutionSnapshotId = 42; let managingBoardStatus: string; + let investorStatus: string; + let contributorStatus: string; let daoRoles: MockContract; let voting: FakeContract; let token: FakeContract; @@ -78,6 +80,8 @@ describe("Resolution", async () => { await resolutionExecutorMock.deployed(); managingBoardStatus = await shareholderRegistry.MANAGING_BOARD_STATUS(); + investorStatus = await shareholderRegistry.INVESTOR_STATUS(); + contributorStatus = await shareholderRegistry.CONTRIBUTOR_STATUS(); resolution = (await upgrades.deployProxy( ResolutionFactory, @@ -1743,9 +1747,8 @@ describe("Resolution", async () => { }); }); - describe("addressable resolution", async () => { - let totalVotingPower = 100; - async function _prepare() { + describe("resolution with exclusion", async () => { + async function _prepare(totalVotingPower = 100) { await resolution .connect(managingBoard) .createResolutionWithExclusion("test", 6, [], [], user2.address); @@ -1771,20 +1774,28 @@ describe("Resolution", async () => { expect(voting.delegateFrom).not.called; }); - it("should remove and re-add delegation when delegating", async () => { + it("should downgrade the contributor to investor and, if delegating, restore status and delegation", async () => { await resolution .connect(managingBoard) .createResolutionWithExclusion("test", 0, [], [], user2.address); voting.getDelegate.whenCalledWith(user2.address).returns(user1.address); + shareholderRegistry.getStatus + .whenCalledWith(user2.address) + .returns(contributorStatus); await resolution.connect(managingBoard).approveResolution(resolutionId); - expect(voting.delegateFrom.getCall(0).args).deep.equal([ + expect(shareholderRegistry.setStatus.getCall(0).args).deep.equal([ + investorStatus, user2.address, + ]); + + expect(shareholderRegistry.setStatus.getCall(1).args).deep.equal([ + contributorStatus, user2.address, ]); - expect(voting.delegateFrom.getCall(1).args).deep.equal([ + expect(voting.delegateFrom.getCall(0).args).deep.equal([ user2.address, user1.address, ]); @@ -1799,87 +1810,6 @@ describe("Resolution", async () => { ).revertedWith("Resolution: account cannot vote"); }); - it("should not count excluded contributor balance for quorum", async () => { - setupUser(user2, 42, 60); - setupUser(user1, 42, 40); - await _prepare(); - - await resolution.connect(user1).vote(resolutionId, true); - - const result = await resolution.getResolutionResult(resolutionId); - - expect(result).to.be.true; - }); - - it("should not count excluded contributor shares for quorum", async () => { - setupUser(user2, 42, 0); - setupUser(user1, 42, 40); - shareholderRegistry.balanceOfAt - .whenCalledWith(user2.address, resolutionSnapshotId) - .returns(60); - await _prepare(); - - await resolution.connect(user1).vote(resolutionId, true); - - const result = await resolution.getResolutionResult(resolutionId); - - expect(result).to.be.true; - }); - - it("should count excluded contributor delegated's balance for quorum", async () => { - setupUser(user2, 42, 60, user1); - setupUser(user1, 42, 40); - await _prepare(); - - await resolution.connect(user1).vote(resolutionId, true); - - const result = await resolution.getResolutionResult(resolutionId); - - expect(result).to.be.true; - }); - - it("should count excluded contributor delegated's shares for quorum", async () => { - setupUser(user2, 42, 60, user1); - setupUser(user1, 42, 0); - shareholderRegistry.balanceOfAt - .whenCalledWith(user1.address, resolutionSnapshotId) - .returns(40); - await _prepare(); - - await resolution.connect(user1).vote(resolutionId, true); - - const result = await resolution.getResolutionResult(resolutionId); - - expect(result).to.be.true; - }); - - it("should count excluded contributor delegator's balance for quorum", async () => { - setupUser(user2, 42, 60); - setupUser(user1, 42, 40, user2); - await _prepare(); - - await resolution.connect(user1).vote(resolutionId, true); - - const result = await resolution.getResolutionResult(resolutionId); - - expect(result).to.be.true; - }); - - it("should count excluded contributor delegator's shares for quorum", async () => { - setupUser(user2, 42, 60); - setupUser(user1, 42, 0, user2); - shareholderRegistry.balanceOfAt - .whenCalledWith(user1.address, resolutionSnapshotId) - .returns(40); - await _prepare(); - - await resolution.connect(user1).vote(resolutionId, true); - - const result = await resolution.getResolutionResult(resolutionId); - - expect(result).to.be.true; - }); - describe("getVoterVote", async () => { it("should fail when asking stats for a user who is excluded", async () => { setupUser(user2, 42, 60); From 0d1d11d71cbadcb6731af2a40945c63da132f73b Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Wed, 20 Sep 2023 11:16:07 +0200 Subject: [PATCH 3/9] Fix double wrap (#120) A call to the `super._mint` function was causing the wrapping of additional tokens which where not supposed to exist. Calling the `ERC20Upgradable` `_mint` function bypasses the additional mint of NEOK tokens. close #119 --- contracts/GovernanceToken/GovernanceToken.sol | 8 +++++++- test/GovernanceToken.ts | 13 +++++++++++-- test/Integration.ts | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/contracts/GovernanceToken/GovernanceToken.sol b/contracts/GovernanceToken/GovernanceToken.sol index 30d6b28..f6dbd15 100644 --- a/contracts/GovernanceToken/GovernanceToken.sol +++ b/contracts/GovernanceToken/GovernanceToken.sol @@ -360,7 +360,7 @@ contract GovernanceToken is Initializable, HasRole, GovernanceTokenSnapshot { DepositedTokens storage tokens = depositedTokens[from][i - 1]; if (block.timestamp >= tokens.settlementTimestamp) { if (tokens.amount > 0) { - super._mint(from, tokens.amount); + ERC20Upgradeable._mint(from, tokens.amount); tokens.amount = 0; } else { break; @@ -368,4 +368,10 @@ contract GovernanceToken is Initializable, HasRole, GovernanceTokenSnapshot { } } } + + function _burnExtra() external onlyRole(Roles.OPERATOR_ROLE) { + // From https://escan.live/tx/0x4973105a8215b74f356b503b1dadfaa7044008c4336ac506d91d1cbbeb56410e + uint256 extraTokens = 7818999911120000000000; + tokenExternal.burn(extraTokens); + } } diff --git a/test/GovernanceToken.ts b/test/GovernanceToken.ts index 8b37d0e..6e79026 100644 --- a/test/GovernanceToken.ts +++ b/test/GovernanceToken.ts @@ -101,6 +101,7 @@ describe("GovernanceToken", () => { redemption.afterMint.reset(); daoRoles.hasRole.reset(); shareholderRegistry.isAtLeast.reset(); + neokingdomToken.mint.reset(); }); describe("transfer hooks", async () => { @@ -335,7 +336,7 @@ describe("GovernanceToken", () => { }); }); - describe("processDepositedTokens", async () => { + describe("settleTokens", async () => { describe("when no tokens have been wrapped", async () => { it("should mint nothing", async () => { await governanceToken.settleTokens(contributor.address); @@ -383,7 +384,7 @@ describe("GovernanceToken", () => { expect(result).equal(41); }); - it("should only not mint non cooled tokens", async () => { + it("should not mint non cooled tokens", async () => { await governanceToken.wrap(contributor.address, 42); await governanceToken.settleTokens(contributor.address); @@ -418,6 +419,14 @@ describe("GovernanceToken", () => { expect(balanceAfter).equal(balanceBefore.add(42)); }); + + it("should not mint an equivalent amount of neok tokens to the governance contract", async () => { + await governanceToken.wrap(contributor.address, 42); + await timeTravel(7); + await governanceToken.settleTokens(contributor.address); + + expect(neokingdomToken.mint).to.not.have.been.called; + }); }); }); diff --git a/test/Integration.ts b/test/Integration.ts index 48e6070..659dede 100644 --- a/test/Integration.ts +++ b/test/Integration.ts @@ -1254,6 +1254,13 @@ describe("Integration", async () => { }); it("back and forth NEOK <-> Governance", async () => { + async function _expectWrapped(count: number) { + let wrappedNEOKs = await neokingdomToken.balanceOf( + governanceToken.address + ); + expect(wrappedNEOKs).equal(count); + } + await governanceToken.setSettlementPeriod(3600 * 24 * 7); const share = parseEther("1"); await shareholderRegistry.mint(user1.address, parseEther("1")); @@ -1261,18 +1268,25 @@ describe("Integration", async () => { await shareholderRegistry.setStatus(contributorStatus, user1.address); await shareholderRegistry.setStatus(contributorStatus, user2.address); + await _expectWrapped(0); + // 15 Governance Tokens to user2 await governanceToken.mint(user2.address, 15); + await _expectWrapped(15); + // 5 Governance tokens minted to user1 await governanceToken.mint(user1.address, 5); + await _expectWrapped(20); // user2 withdraws 10 Governance tokens to user1 await internalMarket.connect(user2).makeOffer(10); await timeTravel(7, true); await internalMarket.connect(user2).withdraw(user1.address, 10); + await _expectWrapped(10); // user1 deposit 4 NEOK await internalMarket.connect(user1).deposit(4); + await _expectWrapped(14); // user1 voting power is 5 expect(await voting.getVotingPower(user1.address)).equal(share.add(5)); // user1 offers 3 NEOK @@ -1287,6 +1301,7 @@ describe("Integration", async () => { ); // user1 deposit 3 NEOK await internalMarket.connect(user1).deposit(3); + await _expectWrapped(17); // user1 voting power is 2 expect(await voting.getVotingPower(user1.address)).equal(share.add(2)); // deposit is finalized @@ -1299,6 +1314,8 @@ describe("Integration", async () => { await governanceToken.settleTokens(user1.address); // user1 voting power is 9 expect(await voting.getVotingPower(user1.address)).equal(share.add(9)); + + await _expectWrapped(17); }); it("internal and external token amounts", async () => { From 6ed9759ba2d573bc5e12cdff9fb4e3f2a4ce11f7 Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Wed, 20 Sep 2023 14:42:23 +0200 Subject: [PATCH 4/9] Upgrade (fix double wrap) (#125) --- .openzeppelin/unknown-9001.json | 316 ++++++++++++++++++++++++++++++-- hardhat.config.ts | 2 +- 2 files changed, 299 insertions(+), 19 deletions(-) diff --git a/.openzeppelin/unknown-9001.json b/.openzeppelin/unknown-9001.json index 5344f1a..4f2516d 100644 --- a/.openzeppelin/unknown-9001.json +++ b/.openzeppelin/unknown-9001.json @@ -2472,7 +2472,7 @@ "label": "_roles", "offset": 0, "slot": "51", - "type": "t_contract(DAORoles)13255", + "type": "t_contract(DAORoles)7607", "contract": "HasRole", "src": "contracts/extensions/HasRole.sol:11" }, @@ -2528,7 +2528,7 @@ "label": "_voting", "offset": 0, "slot": "102", - "type": "t_contract(IVoting)12166", + "type": "t_contract(IVoting)7591", "contract": "GovernanceTokenBase", "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:15" }, @@ -2536,7 +2536,7 @@ "label": "_redemptionController", "offset": 0, "slot": "103", - "type": "t_contract(IRedemptionController)8761", + "type": "t_contract(IRedemptionController)7397", "contract": "GovernanceTokenBase", "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:16" }, @@ -2544,7 +2544,7 @@ "label": "tokenExternal", "offset": 0, "slot": "104", - "type": "t_contract(INeokingdomToken)8400", + "type": "t_contract(INeokingdomToken)7364", "contract": "GovernanceTokenBase", "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:17" }, @@ -2568,7 +2568,7 @@ "label": "_accountBalanceSnapshots", "offset": 0, "slot": "107", - "type": "t_mapping(t_address,t_struct(Snapshots)7016_storage)", + "type": "t_mapping(t_address,t_struct(Snapshots)5954_storage)", "contract": "GovernanceTokenSnapshot", "src": "contracts/GovernanceToken/GovernanceTokenSnapshot.sol:21" }, @@ -2576,7 +2576,7 @@ "label": "_totalSupplySnapshots", "offset": 0, "slot": "108", - "type": "t_struct(Snapshots)7016_storage", + "type": "t_struct(Snapshots)5954_storage", "contract": "GovernanceTokenSnapshot", "src": "contracts/GovernanceToken/GovernanceTokenSnapshot.sol:22" }, @@ -2584,7 +2584,7 @@ "label": "_shareholderRegistry", "offset": 0, "slot": "110", - "type": "t_contract(IShareholderRegistry)11142", + "type": "t_contract(IShareholderRegistry)7494", "contract": "GovernanceToken", "src": "contracts/GovernanceToken/GovernanceToken.sol:20" }, @@ -2592,7 +2592,7 @@ "label": "depositedTokens", "offset": 0, "slot": "111", - "type": "t_mapping(t_address,t_array(t_struct(DepositedTokens)6201_storage)dyn_storage)", + "type": "t_mapping(t_address,t_array(t_struct(DepositedTokens)5139_storage)dyn_storage)", "contract": "GovernanceToken", "src": "contracts/GovernanceToken/GovernanceToken.sol:33" }, @@ -2610,7 +2610,7 @@ "label": "address", "numberOfBytes": "20" }, - "t_array(t_struct(DepositedTokens)6201_storage)dyn_storage": { + "t_array(t_struct(DepositedTokens)5139_storage)dyn_storage": { "label": "struct GovernanceToken.DepositedTokens[]", "numberOfBytes": "32" }, @@ -2630,27 +2630,27 @@ "label": "bool", "numberOfBytes": "1" }, - "t_contract(DAORoles)13255": { + "t_contract(DAORoles)7607": { "label": "contract DAORoles", "numberOfBytes": "20" }, - "t_contract(INeokingdomToken)8400": { + "t_contract(INeokingdomToken)7364": { "label": "contract INeokingdomToken", "numberOfBytes": "20" }, - "t_contract(IRedemptionController)8761": { + "t_contract(IRedemptionController)7397": { "label": "contract IRedemptionController", "numberOfBytes": "20" }, - "t_contract(IShareholderRegistry)11142": { + "t_contract(IShareholderRegistry)7494": { "label": "contract IShareholderRegistry", "numberOfBytes": "20" }, - "t_contract(IVoting)12166": { + "t_contract(IVoting)7591": { "label": "contract IVoting", "numberOfBytes": "20" }, - "t_mapping(t_address,t_array(t_struct(DepositedTokens)6201_storage)dyn_storage)": { + "t_mapping(t_address,t_array(t_struct(DepositedTokens)5139_storage)dyn_storage)": { "label": "mapping(address => struct GovernanceToken.DepositedTokens[])", "numberOfBytes": "32" }, @@ -2658,7 +2658,7 @@ "label": "mapping(address => mapping(address => uint256))", "numberOfBytes": "32" }, - "t_mapping(t_address,t_struct(Snapshots)7016_storage)": { + "t_mapping(t_address,t_struct(Snapshots)5954_storage)": { "label": "mapping(address => struct GovernanceTokenSnapshot.Snapshots)", "numberOfBytes": "32" }, @@ -2670,7 +2670,7 @@ "label": "string", "numberOfBytes": "32" }, - "t_struct(DepositedTokens)6201_storage": { + "t_struct(DepositedTokens)5139_storage": { "label": "struct GovernanceToken.DepositedTokens", "members": [ { @@ -2688,7 +2688,7 @@ ], "numberOfBytes": "64" }, - "t_struct(Snapshots)7016_storage": { + "t_struct(Snapshots)5954_storage": { "label": "struct GovernanceTokenSnapshot.Snapshots", "members": [ { @@ -3386,6 +3386,286 @@ } } } + }, + "8f84054f22cda16e676faba845b0dc14336134dee5ff01b1ac755874f1f8ffa0": { + "address": "0xF28797507FA95A2A093dE9484F5c2E56Ad1b0585", + "txHash": "0x8b227b514473a5039593aafb8a1915f80bb43ae7a154136c6f79abf59e5d7f1e", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_roles", + "offset": 0, + "slot": "51", + "type": "t_contract(DAORoles)7625", + "contract": "HasRole", + "src": "contracts/extensions/HasRole.sol:11" + }, + { + "label": "_balances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:37" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "53", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:39" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "54", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:41" + }, + { + "label": "_name", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:43" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "56", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "__gap", + "offset": 0, + "slot": "57", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:400" + }, + { + "label": "_voting", + "offset": 0, + "slot": "102", + "type": "t_contract(IVoting)7609", + "contract": "GovernanceTokenBase", + "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:15" + }, + { + "label": "_redemptionController", + "offset": 0, + "slot": "103", + "type": "t_contract(IRedemptionController)7415", + "contract": "GovernanceTokenBase", + "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:16" + }, + { + "label": "tokenExternal", + "offset": 0, + "slot": "104", + "type": "t_contract(INeokingdomToken)7382", + "contract": "GovernanceTokenBase", + "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:17" + }, + { + "label": "_vestingBalance", + "offset": 0, + "slot": "105", + "type": "t_mapping(t_address,t_uint256)", + "contract": "GovernanceTokenBase", + "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:28" + }, + { + "label": "_currentSnapshotId", + "offset": 0, + "slot": "106", + "type": "t_uint256", + "contract": "Snapshottable", + "src": "contracts/extensions/Snapshottable.sol:11" + }, + { + "label": "_accountBalanceSnapshots", + "offset": 0, + "slot": "107", + "type": "t_mapping(t_address,t_struct(Snapshots)5972_storage)", + "contract": "GovernanceTokenSnapshot", + "src": "contracts/GovernanceToken/GovernanceTokenSnapshot.sol:21" + }, + { + "label": "_totalSupplySnapshots", + "offset": 0, + "slot": "108", + "type": "t_struct(Snapshots)5972_storage", + "contract": "GovernanceTokenSnapshot", + "src": "contracts/GovernanceToken/GovernanceTokenSnapshot.sol:22" + }, + { + "label": "_shareholderRegistry", + "offset": 0, + "slot": "110", + "type": "t_contract(IShareholderRegistry)7512", + "contract": "GovernanceToken", + "src": "contracts/GovernanceToken/GovernanceToken.sol:20" + }, + { + "label": "depositedTokens", + "offset": 0, + "slot": "111", + "type": "t_mapping(t_address,t_array(t_struct(DepositedTokens)5139_storage)dyn_storage)", + "contract": "GovernanceToken", + "src": "contracts/GovernanceToken/GovernanceToken.sol:33" + }, + { + "label": "settlementPeriod", + "offset": 0, + "slot": "112", + "type": "t_uint256", + "contract": "GovernanceToken", + "src": "contracts/GovernanceToken/GovernanceToken.sol:35" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(DepositedTokens)5139_storage)dyn_storage": { + "label": "struct GovernanceToken.DepositedTokens[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_array(t_uint256)dyn_storage": { + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(DAORoles)7625": { + "label": "contract DAORoles", + "numberOfBytes": "20" + }, + "t_contract(INeokingdomToken)7382": { + "label": "contract INeokingdomToken", + "numberOfBytes": "20" + }, + "t_contract(IRedemptionController)7415": { + "label": "contract IRedemptionController", + "numberOfBytes": "20" + }, + "t_contract(IShareholderRegistry)7512": { + "label": "contract IShareholderRegistry", + "numberOfBytes": "20" + }, + "t_contract(IVoting)7609": { + "label": "contract IVoting", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_array(t_struct(DepositedTokens)5139_storage)dyn_storage)": { + "label": "mapping(address => struct GovernanceToken.DepositedTokens[])", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Snapshots)5972_storage)": { + "label": "mapping(address => struct GovernanceTokenSnapshot.Snapshots)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(DepositedTokens)5139_storage": { + "label": "struct GovernanceToken.DepositedTokens", + "members": [ + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "settlementTimestamp", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Snapshots)5972_storage": { + "label": "struct GovernanceTokenSnapshot.Snapshots", + "members": [ + { + "label": "ids", + "type": "t_array(t_uint256)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "values", + "type": "t_array(t_uint256)dyn_storage", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 4358ba6..ddcb214 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -77,7 +77,7 @@ const config: HardhatUserConfig = { accounts: [TEVMOS_PRIVATE_KEY], }, evmos: { - url: "https://jsonrpc-evmos-ia.cosmosia.notional.ventures/", + url: "https://eth.bd.evmos.org:8545", accounts: [EVMOS_PRIVATE_KEY], }, }, From 4e4c782bf23f03cb2fec5b3c41923a264a1f4eac Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Wed, 20 Sep 2023 15:11:44 +0200 Subject: [PATCH 5/9] Remove cleanup function (#126) close #124 --- .openzeppelin/unknown-9001.json | 280 ++++++++++++++++++ contracts/GovernanceToken/GovernanceToken.sol | 6 - 2 files changed, 280 insertions(+), 6 deletions(-) diff --git a/.openzeppelin/unknown-9001.json b/.openzeppelin/unknown-9001.json index 4f2516d..2e512bf 100644 --- a/.openzeppelin/unknown-9001.json +++ b/.openzeppelin/unknown-9001.json @@ -3666,6 +3666,286 @@ } } } + }, + "246342d7309e3de8809de3d5caa182c103f09298ba3c3d554f7b1920d947b1df": { + "address": "0x0Dd55104F3462f54229672B2027bf74efd732fEB", + "txHash": "0xe961e9a203ed8cd95f96578be128ef5000a6e16dc31cdc233f0b5be21844a766", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_roles", + "offset": 0, + "slot": "51", + "type": "t_contract(DAORoles)7607", + "contract": "HasRole", + "src": "contracts/extensions/HasRole.sol:11" + }, + { + "label": "_balances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:37" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "53", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:39" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "54", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:41" + }, + { + "label": "_name", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:43" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "56", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "__gap", + "offset": 0, + "slot": "57", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:400" + }, + { + "label": "_voting", + "offset": 0, + "slot": "102", + "type": "t_contract(IVoting)7591", + "contract": "GovernanceTokenBase", + "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:15" + }, + { + "label": "_redemptionController", + "offset": 0, + "slot": "103", + "type": "t_contract(IRedemptionController)7397", + "contract": "GovernanceTokenBase", + "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:16" + }, + { + "label": "tokenExternal", + "offset": 0, + "slot": "104", + "type": "t_contract(INeokingdomToken)7364", + "contract": "GovernanceTokenBase", + "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:17" + }, + { + "label": "_vestingBalance", + "offset": 0, + "slot": "105", + "type": "t_mapping(t_address,t_uint256)", + "contract": "GovernanceTokenBase", + "src": "contracts/GovernanceToken/GovernanceTokenBase.sol:28" + }, + { + "label": "_currentSnapshotId", + "offset": 0, + "slot": "106", + "type": "t_uint256", + "contract": "Snapshottable", + "src": "contracts/extensions/Snapshottable.sol:11" + }, + { + "label": "_accountBalanceSnapshots", + "offset": 0, + "slot": "107", + "type": "t_mapping(t_address,t_struct(Snapshots)5954_storage)", + "contract": "GovernanceTokenSnapshot", + "src": "contracts/GovernanceToken/GovernanceTokenSnapshot.sol:21" + }, + { + "label": "_totalSupplySnapshots", + "offset": 0, + "slot": "108", + "type": "t_struct(Snapshots)5954_storage", + "contract": "GovernanceTokenSnapshot", + "src": "contracts/GovernanceToken/GovernanceTokenSnapshot.sol:22" + }, + { + "label": "_shareholderRegistry", + "offset": 0, + "slot": "110", + "type": "t_contract(IShareholderRegistry)7494", + "contract": "GovernanceToken", + "src": "contracts/GovernanceToken/GovernanceToken.sol:20" + }, + { + "label": "depositedTokens", + "offset": 0, + "slot": "111", + "type": "t_mapping(t_address,t_array(t_struct(DepositedTokens)5139_storage)dyn_storage)", + "contract": "GovernanceToken", + "src": "contracts/GovernanceToken/GovernanceToken.sol:33" + }, + { + "label": "settlementPeriod", + "offset": 0, + "slot": "112", + "type": "t_uint256", + "contract": "GovernanceToken", + "src": "contracts/GovernanceToken/GovernanceToken.sol:35" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(DepositedTokens)5139_storage)dyn_storage": { + "label": "struct GovernanceToken.DepositedTokens[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_array(t_uint256)dyn_storage": { + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(DAORoles)7607": { + "label": "contract DAORoles", + "numberOfBytes": "20" + }, + "t_contract(INeokingdomToken)7364": { + "label": "contract INeokingdomToken", + "numberOfBytes": "20" + }, + "t_contract(IRedemptionController)7397": { + "label": "contract IRedemptionController", + "numberOfBytes": "20" + }, + "t_contract(IShareholderRegistry)7494": { + "label": "contract IShareholderRegistry", + "numberOfBytes": "20" + }, + "t_contract(IVoting)7591": { + "label": "contract IVoting", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_array(t_struct(DepositedTokens)5139_storage)dyn_storage)": { + "label": "mapping(address => struct GovernanceToken.DepositedTokens[])", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Snapshots)5954_storage)": { + "label": "mapping(address => struct GovernanceTokenSnapshot.Snapshots)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(DepositedTokens)5139_storage": { + "label": "struct GovernanceToken.DepositedTokens", + "members": [ + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "settlementTimestamp", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Snapshots)5954_storage": { + "label": "struct GovernanceTokenSnapshot.Snapshots", + "members": [ + { + "label": "ids", + "type": "t_array(t_uint256)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "values", + "type": "t_array(t_uint256)dyn_storage", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/contracts/GovernanceToken/GovernanceToken.sol b/contracts/GovernanceToken/GovernanceToken.sol index f6dbd15..1b421ac 100644 --- a/contracts/GovernanceToken/GovernanceToken.sol +++ b/contracts/GovernanceToken/GovernanceToken.sol @@ -368,10 +368,4 @@ contract GovernanceToken is Initializable, HasRole, GovernanceTokenSnapshot { } } } - - function _burnExtra() external onlyRole(Roles.OPERATOR_ROLE) { - // From https://escan.live/tx/0x4973105a8215b74f356b503b1dadfaa7044008c4336ac506d91d1cbbeb56410e - uint256 extraTokens = 7818999911120000000000; - tokenExternal.burn(extraTokens); - } } From b5e785bdcbd41315c644982eb4d1c5cdb81d243e Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Fri, 20 Oct 2023 11:12:16 +0200 Subject: [PATCH 6/9] La audit fixes (#116) - [x] close #90 - [x] close #91 - [x] close #92 - [x] close #93 - [x] close #95 - [x] close #96 - [x] close #97 - [x] close #98 - [x] close #119 --------- Co-authored-by: Alberto Granzotto --- contracts/GovernanceToken/GovernanceToken.sol | 32 +++++--- .../GovernanceToken/GovernanceTokenBase.sol | 8 +- .../GovernanceTokenSnapshot.sol | 3 +- .../GovernanceToken/IGovernanceToken.sol | 3 +- contracts/InternalMarket/InternalMarket.sol | 14 ++-- .../InternalMarket/InternalMarketBase.sol | 3 +- .../NeokingdomToken/INeokingdomToken.sol | 3 +- contracts/NeokingdomToken/NeokingdomToken.sol | 2 +- .../IRedemptionController.sol | 3 +- .../RedemptionController.sol | 7 +- .../RedemptionControllerBase.sol | 2 +- .../ResolutionManager/ResolutionManager.sol | 30 ++------ .../ResolutionManagerBase.sol | 5 +- .../IShareholderRegistry.sol | 3 +- .../ShareholderRegistry.sol | 7 +- .../ShareholderRegistryBase.sol | 16 ++-- .../ShareholderRegistrySnapshot.sol | 4 +- contracts/Voting/IVoting.sol | 3 +- contracts/Voting/Voting.sol | 5 +- contracts/Voting/VotingBase.sol | 2 +- contracts/Voting/VotingSnapshot.sol | 2 +- contracts/extensions/DAORoles.sol | 2 +- contracts/extensions/HasRole.sol | 2 +- contracts/extensions/ISnapshot.sol | 2 +- contracts/extensions/Roles.sol | 2 +- contracts/extensions/Snapshottable.sol | 2 +- contracts/mocks/ERC20Mock.sol | 2 +- contracts/mocks/HasRoleMock.sol | 2 +- contracts/mocks/NeokingdomTokenMock.sol | 2 +- contracts/mocks/NeokingdomTokenV2Mock.sol | 2 +- contracts/mocks/NewTelediskoTokenMock.sol | 2 +- contracts/mocks/ResolutionExecutorMock.sol | 2 +- contracts/mocks/ResolutionManagerV2Mock.sol | 2 +- contracts/mocks/ShareholderRegistryMock.sol | 2 +- contracts/mocks/TokenMock.sol | 2 +- contracts/mocks/VotingMock.sol | 2 +- echidna/proxies/InternalMarketProxy.sol | 2 +- echidna/proxies/RedemptionControllerProxy.sol | 2 +- echidna/proxies/TokenMock.sol | 2 +- hardhat.config.ts | 2 +- test/GovernanceToken.ts | 40 ++++++++++ test/GovernanceTokenSnapshot.ts | 4 + test/Integration.ts | 77 +++++++++++++++++++ test/ResolutionManager.ts | 8 ++ test/ShareholderRegistrySnapshot.ts | 8 ++ 45 files changed, 235 insertions(+), 97 deletions(-) diff --git a/contracts/GovernanceToken/GovernanceToken.sol b/contracts/GovernanceToken/GovernanceToken.sol index 1b421ac..33a69ff 100644 --- a/contracts/GovernanceToken/GovernanceToken.sol +++ b/contracts/GovernanceToken/GovernanceToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; @@ -46,6 +46,10 @@ contract GovernanceToken is Initializable, HasRole, GovernanceTokenSnapshot { string memory name, string memory symbol ) public initializer { + require( + address(roles) != address(0), + "GovernanceToken: 0x0 not allowed" + ); _initialize(name, symbol); _setRoles(roles); } @@ -154,6 +158,15 @@ contract GovernanceToken is Initializable, HasRole, GovernanceTokenSnapshot { uint256 amount ) public virtual onlyRole(Roles.RESOLUTION_ROLE) { _mint(to, amount); + + if ( + _shareholderRegistry.isAtLeast( + _shareholderRegistry.CONTRIBUTOR_STATUS(), + to + ) + ) { + _redemptionController.afterMint(to, amount); + } } /** @@ -292,16 +305,6 @@ contract GovernanceToken is Initializable, HasRole, GovernanceTokenSnapshot { super._afterTokenTransfer(from, to, amount); _voting.afterTokenTransfer(from, to, amount); - if ( - from == address(0) && - _shareholderRegistry.isAtLeast( - _shareholderRegistry.CONTRIBUTOR_STATUS(), - to - ) - ) { - _redemptionController.afterMint(to, amount); - } - // Invariants require( balanceOf(from) >= _vestingBalance[from], @@ -342,7 +345,12 @@ contract GovernanceToken is Initializable, HasRole, GovernanceTokenSnapshot { * @param amount Amount of external tokens to wrap. */ function _wrap(address from, uint amount) internal virtual { - tokenExternal.transferFrom(from, address(this), amount); + require( + tokenExternal.transferFrom(from, address(this), amount), + "GovernanceToken: transfer failed" + ); + require(amount > 0, "GovernanceToken: attempt to wrap 0 tokens"); + uint256 settlementTimestamp = block.timestamp + settlementPeriod; depositedTokens[from].push( DepositedTokens(amount, settlementTimestamp) diff --git a/contracts/GovernanceToken/GovernanceTokenBase.sol b/contracts/GovernanceToken/GovernanceTokenBase.sol index 7b06617..fe33fcc 100644 --- a/contracts/GovernanceToken/GovernanceTokenBase.sol +++ b/contracts/GovernanceToken/GovernanceTokenBase.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "../RedemptionController/IRedemptionController.sol"; @@ -69,7 +68,10 @@ abstract contract GovernanceTokenBase is ERC20Upgradeable, IGovernanceToken { //} function _unwrap(address from, address to, uint amount) internal virtual { - tokenExternal.transfer(to, amount); + require( + tokenExternal.transfer(to, amount), + "GovernanceToken: transfer failed" + ); super._burn(from, amount); } diff --git a/contracts/GovernanceToken/GovernanceTokenSnapshot.sol b/contracts/GovernanceToken/GovernanceTokenSnapshot.sol index 10f7364..0bab57e 100644 --- a/contracts/GovernanceToken/GovernanceTokenSnapshot.sol +++ b/contracts/GovernanceToken/GovernanceTokenSnapshot.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/utils/Arrays.sol"; import "./IGovernanceToken.sol"; diff --git a/contracts/GovernanceToken/IGovernanceToken.sol b/contracts/GovernanceToken/IGovernanceToken.sol index b405745..e910116 100644 --- a/contracts/GovernanceToken/IGovernanceToken.sol +++ b/contracts/GovernanceToken/IGovernanceToken.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "../extensions/ISnapshot.sol"; diff --git a/contracts/InternalMarket/InternalMarket.sol b/contracts/InternalMarket/InternalMarket.sol index 29f472a..9a5bb9e 100644 --- a/contracts/InternalMarket/InternalMarket.sol +++ b/contracts/InternalMarket/InternalMarket.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -23,13 +22,18 @@ contract InternalMarket is Initializable, HasRole, InternalMarketBase { /** * @dev Initializes the contract with the given roles and internal token. * @param roles DAORoles instance containing custom access control roles. - * @param tokenInternal_ Reference to governance token. + * @param governanceToken Reference to governance token. */ function initialize( DAORoles roles, - IGovernanceToken tokenInternal_ + IGovernanceToken governanceToken ) public initializer { - _initialize(tokenInternal_, 7 days); + require( + address(roles) != address(0) && + address(governanceToken) != address(0), + "InternalMarket: 0x0 not allowed" + ); + _initialize(governanceToken, 7 days); _setRoles(roles); } diff --git a/contracts/InternalMarket/InternalMarketBase.sol b/contracts/InternalMarket/InternalMarketBase.sol index 833639d..fec0a3a 100644 --- a/contracts/InternalMarket/InternalMarketBase.sol +++ b/contracts/InternalMarket/InternalMarketBase.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../ShareholderRegistry/IShareholderRegistry.sol"; diff --git a/contracts/NeokingdomToken/INeokingdomToken.sol b/contracts/NeokingdomToken/INeokingdomToken.sol index 2cbce67..aeea5a4 100644 --- a/contracts/NeokingdomToken/INeokingdomToken.sol +++ b/contracts/NeokingdomToken/INeokingdomToken.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/NeokingdomToken/NeokingdomToken.sol b/contracts/NeokingdomToken/NeokingdomToken.sol index f27b126..e2dd53e 100644 --- a/contracts/NeokingdomToken/NeokingdomToken.sol +++ b/contracts/NeokingdomToken/NeokingdomToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity 0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; diff --git a/contracts/RedemptionController/IRedemptionController.sol b/contracts/RedemptionController/IRedemptionController.sol index b281aad..9b23419 100644 --- a/contracts/RedemptionController/IRedemptionController.sol +++ b/contracts/RedemptionController/IRedemptionController.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; diff --git a/contracts/RedemptionController/RedemptionController.sol b/contracts/RedemptionController/RedemptionController.sol index 834c0d3..155c6cc 100644 --- a/contracts/RedemptionController/RedemptionController.sol +++ b/contracts/RedemptionController/RedemptionController.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "./RedemptionControllerBase.sol"; @@ -36,6 +35,10 @@ contract RedemptionController is * @param roles The addresses of DAORoles for this contract. */ function initialize(DAORoles roles) public initializer { + require( + address(roles) != address(0), + "RedemptionController: 0x0 not allowed" + ); _setRoles(roles); _initialize(); } diff --git a/contracts/RedemptionController/RedemptionControllerBase.sol b/contracts/RedemptionController/RedemptionControllerBase.sol index 69883ea..40e7f03 100644 --- a/contracts/RedemptionController/RedemptionControllerBase.sol +++ b/contracts/RedemptionController/RedemptionControllerBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "./IRedemptionController.sol"; diff --git a/contracts/ResolutionManager/ResolutionManager.sol b/contracts/ResolutionManager/ResolutionManager.sol index d0e2b70..0ef7e19 100644 --- a/contracts/ResolutionManager/ResolutionManager.sol +++ b/contracts/ResolutionManager/ResolutionManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { Roles } from "../extensions/Roles.sol"; @@ -26,6 +26,13 @@ contract ResolutionManager is Initializable, ResolutionManagerBase, HasRole { IGovernanceToken governanceToken, IVoting voting ) public initializer { + require( + address(roles) != address(0) && + address(shareholderRegistry) != address(0) && + address(governanceToken) != address(0) && + address(voting) != address(0), + "ResolutionManager: 0x0 not allowed" + ); _setRoles(roles); _initialize(shareholderRegistry, governanceToken, voting); } @@ -167,13 +174,6 @@ contract ResolutionManager is Initializable, ResolutionManagerBase, HasRole { * @param resolutionId The id of the resolution to approve. */ function approveResolution(uint256 resolutionId) external virtual { - require( - _shareholderRegistry.isAtLeast( - _shareholderRegistry.MANAGING_BOARD_STATUS(), - _msgSender() - ), - "Resolution: only managing board can approve" - ); _approveResolution(resolutionId); } @@ -182,13 +182,6 @@ contract ResolutionManager is Initializable, ResolutionManagerBase, HasRole { * @param resolutionId The id of the resolution to reject. */ function rejectResolution(uint256 resolutionId) external virtual { - require( - _shareholderRegistry.isAtLeast( - _shareholderRegistry.MANAGING_BOARD_STATUS(), - _msgSender() - ), - "Resolution: only managing board can reject" - ); _rejectResolution(resolutionId); } @@ -209,13 +202,6 @@ contract ResolutionManager is Initializable, ResolutionManagerBase, HasRole { address[] memory executionTo, bytes[] memory executionData ) external virtual { - require( - _shareholderRegistry.isAtLeast( - _shareholderRegistry.MANAGING_BOARD_STATUS(), - _msgSender() - ), - "Resolution: only managing board can update" - ); _updateResolution( resolutionId, dataURI, diff --git a/contracts/ResolutionManager/ResolutionManagerBase.sol b/contracts/ResolutionManager/ResolutionManagerBase.sol index 4d0e873..819e139 100644 --- a/contracts/ResolutionManager/ResolutionManagerBase.sol +++ b/contracts/ResolutionManager/ResolutionManagerBase.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "../ShareholderRegistry/IShareholderRegistry.sol"; import "../GovernanceToken/IGovernanceToken.sol"; @@ -267,7 +266,7 @@ abstract contract ResolutionManagerBase { bool isNegative, address[] memory executionTo, bytes[] memory executionData - ) internal virtual onlyPending(resolutionId) { + ) internal virtual onlyPending(resolutionId) exists(resolutionId) { emit ResolutionUpdated(msg.sender, resolutionId); Resolution storage resolution = resolutions[resolutionId]; diff --git a/contracts/ShareholderRegistry/IShareholderRegistry.sol b/contracts/ShareholderRegistry/IShareholderRegistry.sol index 7243337..b9236f1 100644 --- a/contracts/ShareholderRegistry/IShareholderRegistry.sol +++ b/contracts/ShareholderRegistry/IShareholderRegistry.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "../extensions/ISnapshot.sol"; diff --git a/contracts/ShareholderRegistry/ShareholderRegistry.sol b/contracts/ShareholderRegistry/ShareholderRegistry.sol index 88d9ada..42265d2 100644 --- a/contracts/ShareholderRegistry/ShareholderRegistry.sol +++ b/contracts/ShareholderRegistry/ShareholderRegistry.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "./ShareholderRegistrySnapshot.sol"; @@ -29,6 +28,10 @@ contract ShareholderRegistry is string memory name, string memory symbol ) public initializer { + require( + address(roles) != address(0), + "ShareholderRegistry: 0x0 not allowed" + ); _initialize(name, symbol); _setRoles(roles); } diff --git a/contracts/ShareholderRegistry/ShareholderRegistryBase.sol b/contracts/ShareholderRegistry/ShareholderRegistryBase.sol index fcf0b88..9df6737 100644 --- a/contracts/ShareholderRegistry/ShareholderRegistryBase.sol +++ b/contracts/ShareholderRegistry/ShareholderRegistryBase.sol @@ -1,11 +1,8 @@ // SPDX-License-Identifier: MIT - -// TODO: update _statuses when account has no shares -// TODO: check who can move shares - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; import "../Voting/IVoting.sol"; contract ShareholderRegistryBase is ERC20Upgradeable { @@ -40,6 +37,10 @@ contract ShareholderRegistryBase is ERC20Upgradeable { } function _setStatus(bytes32 status, address account) internal virtual { + require( + !Address.isContract(account), + "ShareholderRegistry: cannot set status for smart contract" + ); require( status == 0 || isAtLeast(SHAREHOLDER_STATUS, account), "ShareholderRegistry: address has no tokens" @@ -83,7 +84,10 @@ contract ShareholderRegistryBase is ERC20Upgradeable { ) internal view virtual returns (bool) { return balance > 0 && - // shareholder < investor < contributor < managing board + // investor < contributor < managing board + // TODO: shareholder is currently equivalent to investor. + // We need to verify with the lawyer whether we can remove it + // completely from the smart contracts. (status == INVESTOR_STATUS || status == SHAREHOLDER_STATUS || status == accountStatus || diff --git a/contracts/ShareholderRegistry/ShareholderRegistrySnapshot.sol b/contracts/ShareholderRegistry/ShareholderRegistrySnapshot.sol index 6940493..84ee78b 100644 --- a/contracts/ShareholderRegistry/ShareholderRegistrySnapshot.sol +++ b/contracts/ShareholderRegistry/ShareholderRegistrySnapshot.sol @@ -1,7 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/ERC20Snapshot.sol) - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "./ShareholderRegistryBase.sol"; import "../extensions/Snapshottable.sol"; diff --git a/contracts/Voting/IVoting.sol b/contracts/Voting/IVoting.sol index b078a5a..cab918a 100644 --- a/contracts/Voting/IVoting.sol +++ b/contracts/Voting/IVoting.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT - -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "../extensions/ISnapshot.sol"; diff --git a/contracts/Voting/Voting.sol b/contracts/Voting/Voting.sol index d12cd71..000df4f 100644 --- a/contracts/Voting/Voting.sol +++ b/contracts/Voting/Voting.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../ShareholderRegistry/IShareholderRegistry.sol"; @@ -18,6 +18,7 @@ contract Voting is VotingSnapshot, Initializable, HasRole { * @param roles Instance of a DAORoles contract. */ function initialize(DAORoles roles) public initializer { + require(address(roles) != address(0), "Voting: 0x0 not allowed"); _setRoles(roles); } @@ -95,7 +96,7 @@ contract Voting is VotingSnapshot, Initializable, HasRole { /** * @notice Hook called on every governance token transfer. - * @dev Only the governance token can call this method. + * @dev Called by GovernanceToken and ShareholderRegistry. * @param from The sender's address. * @param to The receiver's address. * @param amount The amount transferred. diff --git a/contracts/Voting/VotingBase.sol b/contracts/Voting/VotingBase.sol index 2cb6667..fc37a67 100644 --- a/contracts/Voting/VotingBase.sol +++ b/contracts/Voting/VotingBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "../ShareholderRegistry/IShareholderRegistry.sol"; diff --git a/contracts/Voting/VotingSnapshot.sol b/contracts/Voting/VotingSnapshot.sol index 2b6b407..35c3ac1 100644 --- a/contracts/Voting/VotingSnapshot.sol +++ b/contracts/Voting/VotingSnapshot.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/utils/Arrays.sol"; import "../extensions/Snapshottable.sol"; diff --git a/contracts/extensions/DAORoles.sol b/contracts/extensions/DAORoles.sol index bd8eece..7d2d9c8 100644 --- a/contracts/extensions/DAORoles.sol +++ b/contracts/extensions/DAORoles.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.16; import "@openzeppelin/contracts/access/AccessControl.sol"; diff --git a/contracts/extensions/HasRole.sol b/contracts/extensions/HasRole.sol index 161fc65..2c760d6 100644 --- a/contracts/extensions/HasRole.sol +++ b/contracts/extensions/HasRole.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; diff --git a/contracts/extensions/ISnapshot.sol b/contracts/extensions/ISnapshot.sol index c98c272..0b639ad 100644 --- a/contracts/extensions/ISnapshot.sol +++ b/contracts/extensions/ISnapshot.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; interface ISnapshot { function snapshot() external returns (uint256); diff --git a/contracts/extensions/Roles.sol b/contracts/extensions/Roles.sol index 257c19e..5ca5b28 100644 --- a/contracts/extensions/Roles.sol +++ b/contracts/extensions/Roles.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; library Roles { bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); diff --git a/contracts/extensions/Snapshottable.sol b/contracts/extensions/Snapshottable.sol index 96eca8f..3bd7017 100644 --- a/contracts/extensions/Snapshottable.sol +++ b/contracts/extensions/Snapshottable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/utils/Arrays.sol"; diff --git a/contracts/mocks/ERC20Mock.sol b/contracts/mocks/ERC20Mock.sol index bf5cd38..61e1b11 100644 --- a/contracts/mocks/ERC20Mock.sol +++ b/contracts/mocks/ERC20Mock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/HasRoleMock.sol b/contracts/mocks/HasRoleMock.sol index 6e5bdf7..65912c8 100644 --- a/contracts/mocks/HasRoleMock.sol +++ b/contracts/mocks/HasRoleMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "../extensions/HasRole.sol"; diff --git a/contracts/mocks/NeokingdomTokenMock.sol b/contracts/mocks/NeokingdomTokenMock.sol index 8209f4b..71ffc0f 100644 --- a/contracts/mocks/NeokingdomTokenMock.sol +++ b/contracts/mocks/NeokingdomTokenMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; contract GovernanceTokenMock { mapping(address => uint256) mockResult_balanceOfAt; diff --git a/contracts/mocks/NeokingdomTokenV2Mock.sol b/contracts/mocks/NeokingdomTokenV2Mock.sol index 07e1d30..b65d5f3 100644 --- a/contracts/mocks/NeokingdomTokenV2Mock.sol +++ b/contracts/mocks/NeokingdomTokenV2Mock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "../GovernanceToken/GovernanceToken.sol"; import "../extensions/Roles.sol"; diff --git a/contracts/mocks/NewTelediskoTokenMock.sol b/contracts/mocks/NewTelediskoTokenMock.sol index 2a7aa38..fe4d3cb 100644 --- a/contracts/mocks/NewTelediskoTokenMock.sol +++ b/contracts/mocks/NewTelediskoTokenMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/utils/Arrays.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/ResolutionExecutorMock.sol b/contracts/mocks/ResolutionExecutorMock.sol index 6f97a8e..a9dabb8 100644 --- a/contracts/mocks/ResolutionExecutorMock.sol +++ b/contracts/mocks/ResolutionExecutorMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; contract ResolutionExecutorMock { event MockExecutionSimple(uint256 a); diff --git a/contracts/mocks/ResolutionManagerV2Mock.sol b/contracts/mocks/ResolutionManagerV2Mock.sol index 4e0c5c2..4f60444 100644 --- a/contracts/mocks/ResolutionManagerV2Mock.sol +++ b/contracts/mocks/ResolutionManagerV2Mock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "../ResolutionManager/ResolutionManager.sol"; import "../extensions/Roles.sol"; diff --git a/contracts/mocks/ShareholderRegistryMock.sol b/contracts/mocks/ShareholderRegistryMock.sol index 17c1609..015eee0 100644 --- a/contracts/mocks/ShareholderRegistryMock.sol +++ b/contracts/mocks/ShareholderRegistryMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "../ShareholderRegistry/IShareholderRegistry.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/TokenMock.sol b/contracts/mocks/TokenMock.sol index c97b39b..c6f0b2c 100644 --- a/contracts/mocks/TokenMock.sol +++ b/contracts/mocks/TokenMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index 0cef53c..8634780 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; contract VotingMock { event AfterTokenTransferCalled(address from, address to, uint256 amount); diff --git a/echidna/proxies/InternalMarketProxy.sol b/echidna/proxies/InternalMarketProxy.sol index 361d61a..cf31924 100644 --- a/echidna/proxies/InternalMarketProxy.sol +++ b/echidna/proxies/InternalMarketProxy.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "./TokenMock.sol"; import "../../contracts/InternalMarket/InternalMarket.sol"; diff --git a/echidna/proxies/RedemptionControllerProxy.sol b/echidna/proxies/RedemptionControllerProxy.sol index 2f1c9d9..213e626 100644 --- a/echidna/proxies/RedemptionControllerProxy.sol +++ b/echidna/proxies/RedemptionControllerProxy.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "../../contracts/RedemptionController/RedemptionController.sol"; diff --git a/echidna/proxies/TokenMock.sol b/echidna/proxies/TokenMock.sol index 0ddea86..33cdbb4 100644 --- a/echidna/proxies/TokenMock.sol +++ b/echidna/proxies/TokenMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity 0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/hardhat.config.ts b/hardhat.config.ts index ddcb214..88acca2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -44,7 +44,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.19", + version: "0.8.16", settings: { optimizer: { enabled: true, diff --git a/test/GovernanceToken.ts b/test/GovernanceToken.ts index 6e79026..52b39fe 100644 --- a/test/GovernanceToken.ts +++ b/test/GovernanceToken.ts @@ -94,6 +94,8 @@ describe("GovernanceToken", () => { shareholderRegistry.isAtLeast .whenCalledWith(contributorStatus, contributor2.address) .returns(true); + neokingdomToken.transfer.returns(true); + neokingdomToken.transferFrom.returns(true); }); afterEach(async () => { @@ -101,6 +103,8 @@ describe("GovernanceToken", () => { redemption.afterMint.reset(); daoRoles.hasRole.reset(); shareholderRegistry.isAtLeast.reset(); + neokingdomToken.transfer.reset(); + neokingdomToken.transferFrom.reset(); neokingdomToken.mint.reset(); }); @@ -253,6 +257,19 @@ describe("GovernanceToken", () => { ); }); + it("should fail when the transfer fails", async () => { + neokingdomToken.transferFrom.returns(false); + await expect(governanceToken.wrap(contributor.address, 1)).revertedWith( + "GovernanceToken: transfer failed" + ); + }); + + it("should fail wrapping 0 tokens", async () => { + await expect( + governanceToken.connect(contributor).wrap(contributor.address, 0) + ).revertedWith("GovernanceToken: attempt to wrap 0 tokens"); + }); + it("should transfer external token to itself", async () => { await governanceToken.wrap(contributor.address, 41); expect(neokingdomToken.transferFrom).calledWith( @@ -420,6 +437,22 @@ describe("GovernanceToken", () => { expect(balanceAfter).equal(balanceBefore.add(42)); }); + it("should not mint an equivalent amount of neok tokens to the governance contract", async () => { + neokingdomToken.mint.reset(); + await governanceToken.wrap(contributor.address, 42); + await timeTravel(7); + await governanceToken.settleTokens(contributor.address); + + expect(neokingdomToken.mint).to.not.have.been.called; + }); + + it("should not call RedemptionController.afterMint", async () => { + await governanceToken.wrap(contributor.address, 42); + await timeTravel(7); + await governanceToken.settleTokens(contributor.address); + expect(redemption.afterMint).not.called; + }); + it("should not mint an equivalent amount of neok tokens to the governance contract", async () => { await governanceToken.wrap(contributor.address, 42); await timeTravel(7); @@ -450,6 +483,13 @@ describe("GovernanceToken", () => { ).revertedWith("ERC20: burn amount exceeds balance"); }); + it("should fail when the transfer fails", async () => { + neokingdomToken.transfer.returns(false); + await expect( + governanceToken.unwrap(contributor.address, contributor.address, 1) + ).revertedWith("GovernanceToken: transfer failed"); + }); + it("should transfer external token to 'to' address", async () => { await governanceToken.mint(contributor.address, 41); await governanceToken.unwrap( diff --git a/test/GovernanceTokenSnapshot.ts b/test/GovernanceTokenSnapshot.ts index 5216609..65ab744 100644 --- a/test/GovernanceTokenSnapshot.ts +++ b/test/GovernanceTokenSnapshot.ts @@ -115,11 +115,15 @@ describe("GovernanceTokenSnapshot", () => { beforeEach(async () => { snapshotId = await network.provider.send("evm_snapshot"); daoRoles.hasRole.returns(true); + neokingdomToken.transfer.returns(true); + neokingdomToken.transferFrom.returns(true); }); afterEach(async () => { await network.provider.send("evm_revert", [snapshotId]); daoRoles.hasRole.reset(); + neokingdomToken.transfer.reset(); + neokingdomToken.transferFrom.reset(); }); describe("snapshot logic", async () => { diff --git a/test/Integration.ts b/test/Integration.ts index 659dede..2960394 100644 --- a/test/Integration.ts +++ b/test/Integration.ts @@ -42,6 +42,7 @@ const INITIAL_USDC = 1000; describe("Integration", async () => { let snapshotId: string; let offerDurationDays: number; + let settlementPeriod = 7; let redemptionStartDays: number; let redemptionWindowDays: number; let redemptionMaxDaysInThePast: number; @@ -1130,6 +1131,82 @@ describe("Integration", async () => { ); }); + describe("least authority audit proof of fix (july 2023)", async () => { + it("issue A: Deposited Tokens Can Be Redeemed", async () => { + await _makeContributor(user1, 10); + await _makeContributor(user2, 10); + + // user2 offers 10 tokens + await internalMarket.connect(user2).makeOffer(e(10)); + await timeTravel(offerDurationDays, true); + + // user2 transfers 10 tokens to user1 + await internalMarket.connect(user2).withdraw(user1.address, e(10)); + + // user1 deposit their tokens + await internalMarket.connect(user1).deposit(e(10)); + await timeTravel(settlementPeriod); + await governanceToken.settleTokens(user1.address); + + // user2 offer all tokens, hoping to redeem all of them... + await internalMarket.connect(user1).makeOffer(e(20)); + await timeTravel(redemptionStartDays, true); + await expect(internalMarket.connect(user1).redeem(e(20))).revertedWith( + "Redemption controller: amount exceeds redeemable balance" + ); + + // ...but they can only redeem those that were minted directly to them + await internalMarket.connect(user1).redeem(e(10)); + expect(await tokenMock.balanceOf(user1.address)).equal( + e(INITIAL_USDC + 10) + ); + }); + + it("Issue B: Unsettled Deposits Can Be Locked", async () => { + await _makeContributor(user1, 10); + await _makeContributor(user2, 0); + + // user1 offers token, no one buys + await internalMarket.connect(user1).makeOffer(e(10)); + await timeTravel(offerDurationDays, true); + + // tokens are transferred to user 2 + await internalMarket.connect(user1).withdraw(user2.address, e(10)); + + // user 2 makes 2 deposits, one of which with 0 tokens, which + // could lock the previous deposit forever + await internalMarket.connect(user2).deposit(e(9)); + await expect(internalMarket.connect(user2).deposit(e(0))).revertedWith( + "GovernanceToken: attempt to wrap 0 tokens" + ); + + await timeTravel(settlementPeriod, true); + await governanceToken.settleTokens(user2.address); + + expect(await governanceToken.balanceOf(user2.address)).equal(e(9)); + }); + + it("Issue C: Missing Modifier Preventing the Update of Non-Existent Resolutions", async () => { + const nonExistingResolutionId = 999; + await expect( + resolutionManager + .connect(managingBoard) + .updateResolution(nonExistingResolutionId, "", 0, false, [], []) + ).revertedWith("Resolution: does not exist"); + }); + + it("issue D: the status of internalMarket or shareholderRegistry can be set to contributor status", async () => { + await expect( + shareholderRegistry.setStatus( + contributorStatus, + internalMarket.address + ) + ).revertedWith( + "ShareholderRegistry: cannot set status for smart contract" + ); + }); + }); + it("redemption edge cases", async () => { await _makeContributor(user1, 10); await _makeContributor(user2, 0); diff --git a/test/ResolutionManager.ts b/test/ResolutionManager.ts index 8ecf6ee..58a08be 100644 --- a/test/ResolutionManager.ts +++ b/test/ResolutionManager.ts @@ -306,6 +306,14 @@ describe("Resolution", async () => { ).revertedWith("Resolution: already rejected"); }); + it("doesn't allow the managing board to update a non-existing resolution", async () => { + await expect( + resolution + .connect(managingBoard) + .updateResolution(42, "updated test", 6, true, [], []) + ).revertedWith("Resolution: does not exist"); + }); + it("doesn't allow anyone else to update a resolution", async () => { shareholderRegistry.isAtLeast .whenCalledWith(managingBoardStatus, user1.address) diff --git a/test/ShareholderRegistrySnapshot.ts b/test/ShareholderRegistrySnapshot.ts index df4a749..3c16a89 100644 --- a/test/ShareholderRegistrySnapshot.ts +++ b/test/ShareholderRegistrySnapshot.ts @@ -104,6 +104,14 @@ describe("Shareholder Registry", () => { ).revertedWith("ShareholderRegistry: address has no tokens"); }); + it("should fail if address is a contract", async () => { + await expect( + registry.setStatus(CONTRIBUTOR_STATUS, registry.address) + ).revertedWith( + "ShareholderRegistry: cannot set status for smart contract" + ); + }); + it("should be callable only by a resolution", async () => { await expect( registry.connect(alice).setStatus(CONTRIBUTOR_STATUS, alice.address) From 129ad4a677f0883b61c37668b092f3a079ff1d6f Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Fri, 20 Oct 2023 13:09:11 +0200 Subject: [PATCH 7/9] Add withdrawn event (#129) `withdraw` now emits an event. close #128 --- contracts/InternalMarket/InternalMarketBase.sol | 3 +++ test/InternalMarket.ts | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/contracts/InternalMarket/InternalMarketBase.sol b/contracts/InternalMarket/InternalMarketBase.sol index fec0a3a..c50d73d 100644 --- a/contracts/InternalMarket/InternalMarketBase.sol +++ b/contracts/InternalMarket/InternalMarketBase.sol @@ -17,6 +17,7 @@ contract InternalMarketBase { ); event OfferMatched(uint128 id, address from, address to, uint256 amount); + event Withdrawn(address from, address to, uint256 amount); struct Offer { uint256 expiredAt; @@ -203,6 +204,8 @@ contract InternalMarketBase { } else { tokenInternal.unwrap(from, to, amount); } + + emit Withdrawn(from, to, amount); } function _burn(address from, uint256 amount) internal virtual { diff --git a/test/InternalMarket.ts b/test/InternalMarket.ts index a22626e..2e9f669 100644 --- a/test/InternalMarket.ts +++ b/test/InternalMarket.ts @@ -768,6 +768,13 @@ describe("InternalMarket", async () => { 11 + 25 ); }); + + it("should emit a Withdrawn event", async () => { + await setEVMTimestamp(ts + WEEK + DAY * 3); + expect(internalMarket.connect(alice).withdraw(bob.address, 11)) + .to.emit(internalMarket, "Withdrawn") + .withArgs(alice.address, bob.address, 11); + }); }); describe("match+withdraw", async () => { From 9b6a96d79d8d0611c5be769befe837d3371a4ff5 Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Fri, 20 Oct 2023 17:42:28 +0200 Subject: [PATCH 8/9] Add audit section --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 0ecc7fc..45727c7 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,12 @@ npx hardhat test # Deploy to production npx hardhat deploy --network evmos ``` + +# Audits + +- [SolidProof](https://solidproof.io/) + - Tag: https://github.com/NeokingdomDAO/contracts/releases/tag/audit1 + - Report: https://github.com/solidproof/projects/blob/main/2023/NeokingdomDAO/SmartContract_Audit_Solidproof_NeoKingdomDAO.pdf +- [LeastAuthority](https://leastauthority.com) + - Tag: https://github.com/NeokingdomDAO/contracts/releases/tag/audit2 + - Report: https://leastauthority.com/blog/audits/neokingdom-dao-smart-contracts/ From 4d78acfede49a700026b98c9b3304c250556ee88 Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Fri, 27 Oct 2023 16:08:03 +0200 Subject: [PATCH 9/9] Add events to Redemption Controller (#102) - Add event `RedemptionCreated` for each time a redemption is created (after an offer) - Add event `RedemptionUpdate` for each time the user redeems close #101 --- .../RedemptionControllerBase.sol | 55 ++++++-- test/RedemptionController.ts | 126 +++++++++++++++++- 2 files changed, 172 insertions(+), 9 deletions(-) diff --git a/contracts/RedemptionController/RedemptionControllerBase.sol b/contracts/RedemptionController/RedemptionControllerBase.sol index 40e7f03..4d019b3 100644 --- a/contracts/RedemptionController/RedemptionControllerBase.sol +++ b/contracts/RedemptionController/RedemptionControllerBase.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.16; import "./IRedemptionController.sol"; +import "hardhat/console.sol"; // The contract tells how many tokens are redeemable by Contributors abstract contract RedemptionControllerBase is IRedemptionController { @@ -29,6 +30,21 @@ abstract contract RedemptionControllerBase is IRedemptionController { mapping(address => MintBudget[]) internal _mintBudgets; mapping(address => uint256) internal _mintBudgetsStartIndex; + event RedemptionCreated( + address account, + uint256 index, + uint256 amount, + uint256 starts, + uint256 ends + ); + + event RedemptionUpdated( + address from, + uint256 index, + uint256 amountRequested, + uint256 amountRedeemed + ); + function _initialize() internal { redemptionStart = 60 days; redemptionWindow = 10 days; @@ -69,7 +85,7 @@ abstract contract RedemptionControllerBase is IRedemptionController { } function _afterOffer(address account, uint256 amount) internal virtual { - // Find tokens minted ofer the last 3 months of activity, no earlier than 15 months + // Find tokens minted over the last 3 months of activity, no earlier than 15 months if (_mintBudgets[account].length == 0) { return; } @@ -192,20 +208,36 @@ abstract contract RedemptionControllerBase is IRedemptionController { } } - function _afterRedeem(address account, uint256 amount) internal virtual { + function _afterRedeem( + address account, + uint256 amountRequested + ) internal virtual { Redeemable[] storage redeemables = _redeemables[account]; + uint256 amountLeft = amountRequested; - for (uint256 i = 0; i < redeemables.length && amount > 0; i++) { + for (uint256 i = 0; i < redeemables.length && amountLeft > 0; i++) { Redeemable storage redeemable = redeemables[i]; if ( block.timestamp >= redeemable.start && block.timestamp < redeemable.end ) { - if (amount < redeemable.amount) { - redeemable.amount -= amount; - amount = 0; + if (amountLeft < redeemable.amount) { + redeemable.amount -= amountLeft; + emit RedemptionUpdated( + account, + i, + amountRequested, + amountLeft + ); + amountLeft = 0; } else { - amount -= redeemable.amount; + amountLeft -= redeemable.amount; + emit RedemptionUpdated( + account, + i, + amountRequested, + redeemable.amount + ); redeemable.amount = 0; // FIXME: delete object from array? } @@ -213,7 +245,7 @@ abstract contract RedemptionControllerBase is IRedemptionController { } require( - amount == 0, + amountLeft == 0, "Redemption controller: amount exceeds redeemable balance" ); } @@ -232,6 +264,13 @@ abstract contract RedemptionControllerBase is IRedemptionController { redemptionStarts, redemptionStarts + redemptionWindow ); + emit RedemptionCreated( + account, + _redeemables[account].length, + amount, + offerRedeemable.start, + offerRedeemable.end + ); _redeemables[account].push(offerRedeemable); } } diff --git a/test/RedemptionController.ts b/test/RedemptionController.ts index 0c1488a..6957209 100644 --- a/test/RedemptionController.ts +++ b/test/RedemptionController.ts @@ -12,13 +12,20 @@ import { RedemptionController__factory, } from "../typechain"; -import { mineEVMBlock, timeTravel } from "./utils/evm"; +import { + getEVMTimestamp, + mineEVMBlock, + setEVMTimestamp, + timeTravel, +} from "./utils/evm"; import { roles } from "./utils/roles"; chai.use(solidity); chai.use(chaiAsPromised); const { expect } = chai; +const DAY = 3600 * 24; + describe("RedemptionController", () => { let snapshotId: string; @@ -92,6 +99,90 @@ describe("RedemptionController", () => { `AccessControl: account ${account.address.toLowerCase()} is missing role ${TOKEN_MANAGER_ROLE}` ); }); + + describe("when 10 tokens are minted", async () => { + async function _timestamps() { + const nextTimestamp = (await getEVMTimestamp()) + 1; + await setEVMTimestamp(nextTimestamp); + const expectedStart = nextTimestamp + 60 * DAY; + const expectedEnd = nextTimestamp + 70 * DAY; + + return [expectedStart, expectedEnd]; + } + + beforeEach(async () => { + await redemptionController.afterMint(account.address, 10); + }); + + it("emits a RedeemCreated with 10 tokens and expiration data after offer", async () => { + const [expectedStart, expectedEnd] = await _timestamps(); + await expect(redemptionController.afterOffer(account.address, 10)) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 0, 10, expectedStart, expectedEnd); + }); + + it("emits a RedeemCreated with partially matched mints and expiration data after offer", async () => { + await redemptionController.afterOffer(account.address, 7); + const [expectedStart, expectedEnd] = await _timestamps(); + await expect(redemptionController.afterOffer(account.address, 10)) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 1, 3, expectedStart, expectedEnd); + }); + + it("emits a RedeemCreated from multiple mints with incremental ids", async () => { + await redemptionController.afterMint(account.address, 12); + const [expectedStart, expectedEnd] = await _timestamps(); + await expect(redemptionController.afterOffer(account.address, 22)) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 0, 10, expectedStart, expectedEnd) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 1, 12, expectedStart, expectedEnd); + }); + + it("emits a RedeemCreated from expired redemptions", async () => { + await redemptionController.afterOffer(account.address, 10); + await timeTravel(70, true); // redemption expires + const [expectedStart, expectedEnd] = await _timestamps(); + await expect(redemptionController.afterOffer(account.address, 10)) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 1, 10, expectedStart, expectedEnd); + }); + + it("emits a RedeemCreated from partially matched expired redemptions", async () => { + await redemptionController.afterOffer(account.address, 10); + await timeTravel(60, true); // redemption starts + await redemptionController.afterRedeem(account.address, 7); + await timeTravel(10, true); // redemption starts + const [expectedStart, expectedEnd] = await _timestamps(); + await expect(redemptionController.afterOffer(account.address, 10)) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 1, 3, expectedStart, expectedEnd); + }); + + it("emits a RedeemCreated from multiple expired redemptions, partially", async () => { + await redemptionController.afterOffer(account.address, 6); + await redemptionController.afterOffer(account.address, 4); + await timeTravel(70, true); // redemption expires + const [expectedStart, expectedEnd] = await _timestamps(); + await expect(redemptionController.afterOffer(account.address, 9)) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 2, 6, expectedStart, expectedEnd) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 3, 3, expectedStart, expectedEnd); + }); + + it("emits a RedeemCreated from multiple expired redemptions, completely", async () => { + await redemptionController.afterOffer(account.address, 6); + await redemptionController.afterOffer(account.address, 4); + await timeTravel(70, true); // redemption expires + const [expectedStart, expectedEnd] = await _timestamps(); + await expect(redemptionController.afterOffer(account.address, 15)) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 2, 6, expectedStart, expectedEnd) + .to.emit(redemptionController, "RedemptionCreated") + .withArgs(account.address, 3, 4, expectedStart, expectedEnd); + }); + }); }); describe("afterRedeem", async () => { @@ -110,6 +201,39 @@ describe("RedemptionController", () => { "Redemption controller: amount exceeds redeemable balance" ); }); + + describe.only("when 10 tokens are redeemable", async () => { + beforeEach(async () => { + await redemptionController.afterMint(account.address, 10); + await redemptionController.afterOffer(account.address, 10); + await timeTravel(60, true); + }); + + it("emits a RedeemUpdated upon full redemption", async () => { + await expect(redemptionController.afterRedeem(account.address, 10)) + .to.emit(redemptionController, "RedemptionUpdated") + .withArgs(account.address, 0, 10, 10); + }); + + it("emits a RedeemUpdated upon partial redemption", async () => { + await expect(redemptionController.afterRedeem(account.address, 7)) + .to.emit(redemptionController, "RedemptionUpdated") + .withArgs(account.address, 0, 7, 7); + }); + + it("emits a RedeemUpdated upon partial redemption for each redemption covering the amount requested", async () => { + await timeTravel(10, true); // expire old redemption + // make two redemptions + await redemptionController.afterOffer(account.address, 7); + await redemptionController.afterOffer(account.address, 3); + await timeTravel(60, true); // redemption time + await expect(redemptionController.afterRedeem(account.address, 10)) + .to.emit(redemptionController, "RedemptionUpdated") + .withArgs(account.address, 1, 10, 7) + .to.emit(redemptionController, "RedemptionUpdated") + .withArgs(account.address, 2, 10, 3); + }); + }); }); describe("redeemableBalance", async () => {