From 4f6c971f0ff148cda840f26b382653341c5f8c42 Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Fri, 11 Aug 2023 16:08:26 +0200 Subject: [PATCH 1/2] 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/2] 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);