diff --git a/packages/protocol/contracts-0.8/common/GasPriceMinimum.sol b/packages/protocol/contracts-0.8/common/GasPriceMinimum.sol index f09dc6f8859..bc08d26c9d6 100644 --- a/packages/protocol/contracts-0.8/common/GasPriceMinimum.sol +++ b/packages/protocol/contracts-0.8/common/GasPriceMinimum.sol @@ -9,6 +9,7 @@ import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; import "../../contracts/common/FixidityLib.sol"; import "./UsingRegistry.sol"; import "../../contracts/stability/interfaces/ISortedOracles.sol"; +import "@openzeppelin/contracts8/utils/math/Math.sol"; /** * @title Stores and provides gas price minimum for various currencies. @@ -38,6 +39,7 @@ contract GasPriceMinimum is FixidityLib.Fraction public adjustmentSpeed; uint256 public baseFeeOpCodeActivationBlock; + uint256 public constant ABSOLUTE_MINIMAL_GAS_PRICE = 1; /** * @notice Sets initialized == true on implementation contracts @@ -53,7 +55,7 @@ contract GasPriceMinimum is * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 2, 0, 0); + return (1, 2, 0, 1); } /** @@ -150,12 +152,7 @@ contract GasPriceMinimum is } } - /** - * @notice Retrieve the current gas price minimum for a currency. - * @param tokenAddress The currency the gas price should be in (defaults to gold). - * @return current gas price minimum in the requested currency - */ - function getGasPriceMinimum(address tokenAddress) external view returns (uint256) { + function _getGasPriceMinimum(address tokenAddress) private view returns (uint256) { if ( tokenAddress == address(0) || tokenAddress == registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID) @@ -172,6 +169,21 @@ contract GasPriceMinimum is } } + /** + * @notice Retrieve the current gas price minimum for a currency. + * When caled for 0x0 or Celo address, it returns gasPriceMinimum(). + * For other addresses it returns gasPriceMinimum() mutiplied by + * the SortedOracles median of the token. It does not check tokenAddress is a valid fee currency. + * this function will never returns values less than ABSOLUTE_MINIMAL_GAS_PRICE. + * If Oracle rate doesn't exist, it returns ABSOLUTE_MINIMAL_GAS_PRICE. + * @dev This functions assumes one unit of token has 18 digits. + * @param tokenAddress The currency the gas price should be in (defaults to Celo). + * @return current gas price minimum in the requested currency + */ + function getGasPriceMinimum(address tokenAddress) external view returns (uint256) { + return Math.max(_getGasPriceMinimum(tokenAddress), ABSOLUTE_MINIMAL_GAS_PRICE); + } + /** * @notice Adjust the gas price minimum based on governable parameters * and block congestion. diff --git a/packages/protocol/contracts-0.8/stability/CeloFeeCurrencyAdapterOwnable.sol b/packages/protocol/contracts-0.8/stability/CeloFeeCurrencyAdapterOwnable.sol new file mode 100644 index 00000000000..a3a685014ec --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/CeloFeeCurrencyAdapterOwnable.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "./FeeCurrencyAdapterOwnable.sol"; + +contract CeloFeeCurrencyAdapterOwnable is FeeCurrencyAdapterOwnable { + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) FeeCurrencyAdapterOwnable(test) {} + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } +} diff --git a/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol new file mode 100644 index 00000000000..1a7980b9625 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/access/Ownable.sol"; +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +import "../../contracts/common/CalledByVm.sol"; +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts/common/FixidityLib.sol"; +import "../../contracts/stability/interfaces/ISortedOracles.sol"; +import "./interfaces/IFeeCurrency.sol"; +import "./interfaces/IDecimals.sol"; +import "./interfaces/IFeeCurrencyAdapter.sol"; + +contract FeeCurrencyAdapter is Initializable, CalledByVm, IFeeCurrencyAdapter { + IFeeCurrency public adaptedToken; + + uint96 public digitDifference; + + uint256 public debited = 0; + + string public name; + string public symbol; + + uint8 public expectedDecimals; + + uint256[44] __gap; + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param _adaptedToken The address of the adapted token. + * @param _name The name of the adapted token. + * @param _symbol The symbol of the adapted token. + * @param _expectedDecimals The expected number of decimals of the adapted token. + * @notice _expectedDecimals must be bigger than _adaptedToken.decimals(). + */ + function initialize( + address _adaptedToken, + string memory _name, + string memory _symbol, + uint8 _expectedDecimals + ) public virtual initializer { + _setAdaptedToken(_adaptedToken); + name = _name; + symbol = _symbol; + uint8 _decimals = IDecimals(_adaptedToken).decimals(); + require( + _decimals < _expectedDecimals, + "Decimals of adapted token must be < expected decimals." + ); + digitDifference = uint96(10**(_expectedDecimals - _decimals)); + expectedDecimals = _expectedDecimals; + } + + /** + * Downscales value to the adapted token's native digits and debits it. + * @param from from address + * @param value Debited value in the adapted digits. + */ + function debitGasFees(address from, uint256 value) external onlyVm { + uint256 valueScaled = downscale(value); + require(valueScaled > 0, "Scaled debit value must be > 0."); + debited = valueScaled; + adaptedToken.debitGasFees(from, valueScaled); + } + + /** + * Downscales value to the adapted token's native digits and credits it. + * @param refundRecipient The recipient of the refund. + * @param tipRecipient The recipient of the tip. + * @param _gatewayFeeRecipient The recipient of the gateway fee. Unused. + * @param baseFeeRecipient The recipient of the base fee. + * @param refundAmount The amount to refund (in adapted token digits). + * @param tipAmount The amount to tip (in adapted token digits). + * @param _gatewayFeeAmount The amount of the gateway fee (in adapted token digits). Unused. + * @param baseFeeAmount The amount of the base fee (in adapted token digits). + */ + function creditGasFees( + address refundRecipient, + address tipRecipient, + address _gatewayFeeRecipient, + address baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256 _gatewayFeeAmount, + uint256 baseFeeAmount + ) external onlyVm { + if (debited == 0) { + // When eth.estimateGas is called, this function is called but we don't want to credit anything. + return; + } + + uint256 refundScaled = downscale(refundAmount); + uint256 tipTxFeeScaled = downscale(tipAmount); + uint256 baseTxFeeScaled = downscale(baseFeeAmount); + + require( + refundScaled + tipTxFeeScaled + baseTxFeeScaled <= debited, + "Cannot credit more than debited." + ); + + uint256 roundingError = debited - (refundScaled + tipTxFeeScaled + baseTxFeeScaled); + + if (roundingError > 0) { + baseTxFeeScaled += roundingError; + } + adaptedToken.creditGasFees( + refundRecipient, + tipRecipient, + address(0), + baseFeeRecipient, + refundScaled, + tipTxFeeScaled, + 0, + baseTxFeeScaled + ); + + debited = 0; + } + + /** + * @notice Returns adapted token address. + * @return The adapted token address. + */ + function getAdaptedToken() external view returns (address) { + return address(adaptedToken); + } + + /** + * @notice Gets the balance of the specified address with correct digits. + * @param account The address to query the balance of. + * @return The balance of the specified address. + */ + function balanceOf(address account) external view returns (uint256) { + return upscale(adaptedToken.balanceOf(account)); + } + + /** + * @notice Gets the total supply with correct digits. + * @return The total supply. + */ + function totalSupply() external view returns (uint256) { + return upscale(adaptedToken.totalSupply()); + } + + /** + * @notice Gets the total supply with correct digits. + * @return The total supply. + */ + function decimals() external view returns (uint8) { + return expectedDecimals; + } + + function upscale(uint256 value) internal view returns (uint256) { + return value * digitDifference; + } + + /** + * @notice Downscales value to the adapted token's native digits. + * @dev Downscale is rounding up in favour of protocol. User possibly can pay a bit more than expected (up to 1 unit of a token). + * Example: + * USDC has 6 decimals and in such case user can pay up to 0.000001 USDC more than expected. + * WBTC (currently not supported by Celo chain as fee currency) has 8 decimals and in such case user can pay up to 0.00000001 WBTC more than expected. + * Considering the current price of WBTC, it's less than 0.0005 USD. Even when WBTC price would be 1 mil USD, it's still would be only 0.01 USD. + * In general it is a very small amount and it is acceptable to round up in favor of the protocol. + * @param value The value to downscale. + */ + function downscale(uint256 value) internal view returns (uint256) { + return (value + digitDifference - 1) / digitDifference; + } + + function _setAdaptedToken(address _adaptedToken) internal virtual { + adaptedToken = IFeeCurrency(_adaptedToken); + } +} diff --git a/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapterOwnable.sol b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapterOwnable.sol new file mode 100644 index 00000000000..54333f11908 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapterOwnable.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/access/Ownable.sol"; +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +import "./FeeCurrencyAdapter.sol"; + +contract FeeCurrencyAdapterOwnable is FeeCurrencyAdapter, Ownable { + uint256[49] __gap2; + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) FeeCurrencyAdapter(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param _adaptedToken The address of the adapted token. + * @param _name The name of the adapted token. + * @param _symbol The symbol of the adapted token. + * @param _expectedDecimals The expected number of decimals of the adapted token. + */ + function initialize( + address _adaptedToken, + string memory _name, + string memory _symbol, + uint8 _expectedDecimals + ) public override { + _transferOwnership(msg.sender); + FeeCurrencyAdapter.initialize(_adaptedToken, _name, _symbol, _expectedDecimals); + } + + /** + * @notice Sets adapted token address. + * @param _adaptedToken The address of the adapted token. + */ + function setAdaptedToken(address _adaptedToken) public onlyOwner { + _setAdaptedToken(_adaptedToken); + } +} diff --git a/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol b/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol new file mode 100644 index 00000000000..2b68b9f8060 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.13; + +interface IDecimals { + function decimals() external view returns (uint8); +} diff --git a/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol new file mode 100644 index 00000000000..e8c800a3c93 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol @@ -0,0 +1,58 @@ +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +interface IFeeCurrency is IERC20 { + /* + This interface should be implemented for tokens which are supposed to + act as fee currencies on the Celo blockchain, meaning that they can be + used to pay gas fees for CIP-64 transactions (and some older tx types). + See https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0064.md + + Before executing a tx with non-empty feeCurrency field, the fee + currency's `debitGasFees` function is called to reserve the maximum + amount that tx can spend on gas. After the tx has been executed, the + `creditGasFees` function is called to refund the unused gas and credit + the spent fees to the correct recipients. Events which are raised inside + these functions will show up for every transaction using the token as a + fee currency. + + Requirements: + - The functions will be called by the blockchain client with `msg.sender + == address(0)`. If this condition is not met, the functions must + revert to prevent malicious users from crediting their accounts directly. + - `creditGasFees` must credit all specified amounts. If it impossible to + credit one of the recipients for some reason, add the amount to the + value credited to the first valid recipient. This is important to keep + the debited and credited amounts consistent. + */ + + // Called before transaction execution to reserve the maximum amount of gas + // that can be used by the transaction. + // - The implementation must reduce `from`'s balance by `value`. + // - Must revert if `msg.sender` is not the zero address. + function debitGasFees(address from, uint256 value) external; + + /** + * Called after transaction execution to refund the unused gas and credit the + * spent fees to the correct recipients. + * @param refundRecipient The recipient of the refund. + * @param tipRecipient The recipient of the tip. + * @param _gatewayFeeRecipient The recipient of the gateway fee. Unused. + * @param baseFeeRecipient The recipient of the base fee. + * @param refundAmount The amount to refund. + * @param tipAmount The amount to tip. + * @param _gatewayFeeAmount The amount of the gateway fee. Unused. + * @param baseFeeAmount The amount of the base fee. + */ + function creditGasFees( + address refundRecipient, + address tipRecipient, + address _gatewayFeeRecipient, + address baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256 _gatewayFeeAmount, + uint256 baseFeeAmount + ) external; +} diff --git a/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrencyAdapter.sol b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrencyAdapter.sol new file mode 100644 index 00000000000..1bf84385e7a --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrencyAdapter.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +interface IFeeCurrencyAdapter { + /** + * @return The address of the adapted token. + */ + function getAdaptedToken() external view returns (address); + + /** + * @return The multiplier that should be used when upscaling and downscaling. This is the result of 10**(expectedDecimals - getAdaptedToken().decimals()). + */ + function digitDifference() external view returns (uint96); + + /** + * @return The amount that is debited after calling debitGasFees() and before creditGasFees(). + */ + function debited() external view returns (uint256); + + /** + * @return The name of the adapted token. + */ + function name() external view returns (string memory); + + /** + * @return The symbol of adapted token. + */ + function symbol() external view returns (string memory); + + /** + * @return The decimals expected by the vm. + */ + function decimals() external view returns (uint8); + + /** + * @return Same as decimals. + */ + function expectedDecimals() external view returns (uint8); + + /** + * @notice Same as debitGasFees in IFeeCurrency, always using the number of decimals the evm expects. + */ + function debitGasFees(address from, uint256 value) external; + + /** + * @notice Same as creditGasFees in IFeeCurrency, always using the number of decimals the evm expects. + */ + function creditGasFees( + address refundRecipient, + address tipRecipient, + address _gatewayFeeRecipient, + address baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256 _gatewayFeeAmount, + uint256 baseFeeAmount + ) external; +} diff --git a/packages/protocol/contracts/common/proxies/FeeCurrencyAdapterProxy.sol b/packages/protocol/contracts/common/proxies/FeeCurrencyAdapterProxy.sol new file mode 100644 index 00000000000..8141d95d0f2 --- /dev/null +++ b/packages/protocol/contracts/common/proxies/FeeCurrencyAdapterProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract FeeCurrencyAdapterProxy is Proxy {} diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 633b7d2d23c..f14744c462b 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -1471,7 +1471,8 @@ contract Governance is uint256 maxUsed = 0; for (uint256 index = 0; index < dequeued.length; index = index.add(1)) { - Proposals.Proposal storage proposal = proposals[dequeued[index]]; + uint256 proposalId = dequeued[index]; + Proposals.Proposal storage proposal = proposals[proposalId]; bool isVotingReferendum = (getProposalDequeuedStage(proposal) == Proposals.Stage.Referendum); if (!isVotingReferendum) { @@ -1479,6 +1480,11 @@ contract Governance is } VoteRecord storage voteRecord = voter.referendumVotes[index]; + // skip if vote record is not for this proposal + if (voteRecord.proposalId != proposalId) { + continue; + } + uint256 votesCast = voteRecord.yesVotes.add(voteRecord.noVotes).add(voteRecord.abstainVotes); maxUsed = Math.max( maxUsed, @@ -1514,7 +1520,8 @@ contract Governance is Voter storage voter = voters[account]; for (uint256 index = 0; index < dequeued.length; index = index.add(1)) { - Proposals.Proposal storage proposal = proposals[dequeued[index]]; + uint256 proposalId = dequeued[index]; + Proposals.Proposal storage proposal = proposals[proposalId]; bool isVotingReferendum = (getProposalDequeuedStage(proposal) == Proposals.Stage.Referendum); if (!isVotingReferendum) { @@ -1522,6 +1529,13 @@ contract Governance is } VoteRecord storage voteRecord = voter.referendumVotes[index]; + + // skip if vote record is not for this proposal + if (voteRecord.proposalId != proposalId) { + delete voter.referendumVotes[index]; + continue; + } + uint256 sumOfVotes = voteRecord.yesVotes.add(voteRecord.noVotes).add(voteRecord.abstainVotes); if (sumOfVotes > newVotingPower) { diff --git a/packages/protocol/contracts/stability/SortedOracles.sol b/packages/protocol/contracts/stability/SortedOracles.sol index 813d923127a..35cb4e5865c 100644 --- a/packages/protocol/contracts/stability/SortedOracles.sol +++ b/packages/protocol/contracts/stability/SortedOracles.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; @@ -5,6 +6,7 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/ISortedOracles.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; +import "./interfaces/IBreakerBox.sol"; import "../common/FixidityLib.sol"; import "../common/Initializable.sol"; @@ -12,18 +14,41 @@ import "../common/linkedlists/AddressSortedLinkedListWithMedian.sol"; import "../common/linkedlists/SortedLinkedListWithMedian.sol"; /** - * @title Maintains a sorted list of oracle exchange rates between CELO and other currencies. + * @title SortedOracles + * + * @notice This contract stores a collection of exchange rates with CELO + * expressed in units of other assets. The most recent exchange rates + * are gathered off-chain by oracles, who then use the `report` function to + * submit the rates to this contract. Before submitting a rate report, an + * oracle's address must be added to the `isOracle` mapping for a specific + * rateFeedId, with the flag set to true. While submitting a report requires + * an address to be added to the mapping, no additional permissions are needed + * to read the reports, the calculated median rate, or the list of oracles. + * + * @dev A unique rateFeedId identifies each exchange rate. In the initial implementation + * of this contract, the rateFeedId was set as the address of the stable + * asset contract that used the rate. However, this implementation has since + * been updated, and the rateFeedId now also refers to an address derived from the + * concatenation other asset symbols. This change enables the contract to store multiple exchange rates for a + * single token. As a result of this change, there may be instances + * where the term "token" is used in the contract code. These useages of the term + * "token" are actually referring to the rateFeedId. + * */ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initializable { using SafeMath for uint256; using AddressSortedLinkedListWithMedian for SortedLinkedListWithMedian.List; using FixidityLib for FixidityLib.Fraction; - uint256 private constant FIXED1_UINT = 1000000000000000000000000; + struct EquivalentToken { + address token; + } + + uint256 private constant FIXED1_UINT = 1e24; - // Maps a token address to a sorted list of report values. + // Maps a rateFeedID to a sorted list of report values. mapping(address => SortedLinkedListWithMedian.List) private rates; - // Maps a token address to a sorted list of report timestamps. + // Maps a rateFeedID to a sorted list of report timestamps. mapping(address => SortedLinkedListWithMedian.List) private timestamps; mapping(address => mapping(address => bool)) public isOracle; mapping(address => address[]) public oracles; @@ -34,8 +59,14 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi // doesn't have a value in the mapping (i.e. it's 0), the fallback is used. // See: #getTokenReportExpirySeconds uint256 public reportExpirySeconds; + // Maps a rateFeedId to its report expiry time in seconds. mapping(address => uint256) public tokenReportExpirySeconds; + IBreakerBox public breakerBox; + // Maps a token address to its equivalent token address. + // Original token will return the median value same as the value of equivalent token. + mapping(address => EquivalentToken) public equivalentTokens; + event OracleAdded(address indexed token, address indexed oracleAddress); event OracleRemoved(address indexed token, address indexed oracleAddress); event OracleReported( @@ -48,6 +79,8 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi event MedianUpdated(address indexed token, uint256 value); event ReportExpirySet(uint256 reportExpiry); event TokenReportExpirySet(address token, uint256 reportExpiry); + event BreakerBoxUpdated(address indexed newBreakerBox); + event EquivalentTokenSet(address indexed token, address indexed equivalentToken); modifier onlyOracle(address token) { require(isOracle[token][msg.sender], "sender was not an oracle for token addr"); @@ -62,7 +95,7 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 2, 3); + return (1, 1, 3, 0); } /** @@ -92,8 +125,8 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi } /** - * @notice Sets the report expiry parameter for a token. - * @param _token The address of the token to set expiry for. + * @notice Sets the report expiry parameter for a rateFeedId. + * @param _token The token for which the report expiry is being set. * @param _reportExpirySeconds The number of seconds before a report is considered expired. */ function setTokenReportExpiry(address _token, uint256 _reportExpirySeconds) external onlyOwner { @@ -107,26 +140,39 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi } /** - * @notice Adds a new Oracle. - * @param token The address of the token. + * @notice Sets the address of the BreakerBox. + * @param newBreakerBox The new BreakerBox address. + */ + function setBreakerBox(IBreakerBox newBreakerBox) public onlyOwner { + require(address(newBreakerBox) != address(0), "BreakerBox address must be set"); + breakerBox = newBreakerBox; + emit BreakerBoxUpdated(address(newBreakerBox)); + } + + /** + * @notice Adds a new Oracle for a specified rate feed. + * @param token The token for which the specified oracle is to be added. * @param oracleAddress The address of the oracle. */ function addOracle(address token, address oracleAddress) external onlyOwner { - require(token != address(0), "token addr was null"); - require(oracleAddress != address(0), "oracle addr was null"); - require(!isOracle[token][oracleAddress], "oracle addr is not an oracle for token addr"); + // solhint-disable-next-line reason-string + require( + token != address(0) && oracleAddress != address(0) && !isOracle[token][oracleAddress], + "token addr was null or oracle addr was null or oracle addr is already an oracle for token addr" + ); isOracle[token][oracleAddress] = true; oracles[token].push(oracleAddress); emit OracleAdded(token, oracleAddress); } /** - * @notice Removes an Oracle. - * @param token The address of the token. + * @notice Removes an Oracle from a specified rate feed. + * @param token The token from which the specified oracle is to be removed. * @param oracleAddress The address of the oracle. * @param index The index of `oracleAddress` in the list of oracles. */ function removeOracle(address token, address oracleAddress, uint256 index) external onlyOwner { + // solhint-disable-next-line reason-string require( token != address(0) && oracleAddress != address(0) && @@ -136,7 +182,7 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi ); isOracle[token][oracleAddress] = false; oracles[token][index] = oracles[token][oracles[token].length.sub(1)]; - oracles[token].length = oracles[token].length.sub(1); + oracles[token].pop(); if (reportExists(token, oracleAddress)) { removeReport(token, oracleAddress); } @@ -145,7 +191,7 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi /** * @notice Removes a report that is expired. - * @param token The address of the token for which the CELO exchange rate is being reported. + * @param token The token for which the expired report is to be removed. * @param n The number of expired reports to remove, at most (deterministic upper gas bound). */ function removeExpiredReports(address token, uint256 n) external { @@ -165,12 +211,13 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi /** * @notice Check if last report is expired. - * @param token The address of the token for which the CELO exchange rate is being reported. - * @return isExpired - * @return The address of the last report. + * @param token The token for which the expired report is to be checked. + * @return bool A bool indicating if the last report is expired. + * @return address Oracle address of the last report. */ function isOldestReportExpired(address token) public view returns (bool, address) { - require(token != address(0), "token address cannot be null"); + // solhint-disable-next-line reason-string + require(token != address(0)); address oldest = timestamps[token].getTail(); uint256 timestamp = timestamps[token].getValue(oldest); // solhint-disable-next-line not-rely-on-time @@ -180,10 +227,42 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi return (false, oldest); } + /** + * @notice Sets the equivalent token for a token. + * @param token The address of the token. + * @param equivalentToken The address of the equivalent token. + */ + function setEquivalentToken(address token, address equivalentToken) external onlyOwner { + require(token != address(0), "token address cannot be 0"); + require(equivalentToken != address(0), "equivalentToken address cannot be 0"); + equivalentTokens[token] = EquivalentToken(equivalentToken); + emit EquivalentTokenSet(token, equivalentToken); + } + + /** + * @notice Sets the equivalent token for a token. + * @param token The address of the token. + */ + function deleteEquivalentToken(address token) external onlyOwner { + require(token != address(0), "token address cannot be 0"); + delete equivalentTokens[token]; + emit EquivalentTokenSet(token, address(0)); + } + + /** + * @notice Gets the equivalent token for a token. + * @param token The address of the token. + * @return The address of the equivalent token. + */ + function getEquivalentToken(address token) external view returns (address) { + return (equivalentTokens[token].token); + } + /** * @notice Updates an oracle value and the median. - * @param token The address of the token for which the CELO exchange rate is being reported. - * @param value The amount of `token` equal to one CELO, expressed as a fixidity value. + * @param token The token for which the rate is being reported. + * @param value The number of stable asset that equate to one unit of collateral asset, for the + * specified rateFeedId, expressed as a fixidity value. * @param lesserKey The element which should be just left of the new oracle value. * @param greaterKey The element which should be just right of the new oracle value. * @dev Note that only one of `lesserKey` or `greaterKey` needs to be correct to reduce friction. @@ -222,31 +301,64 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi if (newMedian != originalMedian) { emit MedianUpdated(token, newMedian); } + + if (address(breakerBox) != address(0)) { + breakerBox.checkAndSetBreakers(token); + } } /** - * @notice Returns the number of rates. - * @param token The address of the token for which the CELO exchange rate is being reported. - * @return The number of reported oracle rates for `token`. + * @notice Returns the number of rates that are currently stored for a specifed rateFeedId. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the number of rates is being retrieved. + * @return uint256 The number of reported oracle rates stored for the given rateFeedId. */ function numRates(address token) public view returns (uint256) { return rates[token].getNumElements(); } /** - * @notice Returns the median rate. - * @param token The address of the token for which the CELO exchange rate is being reported. - * @return The median exchange rate for `token`. - * @return fixidity + * @notice Returns the median of the currently stored rates for a specified rateFeedId. + * @dev Does not take the equivalentTokens mapping into account. + * @param token The token for which the median value is being retrieved. + * @return uint256 The median exchange rate for rateFeedId (fixidity). + * @return uint256 denominator */ - function medianRate(address token) external view returns (uint256, uint256) { + function medianRateWithoutEquivalentMapping(address token) + public + view + returns (uint256, uint256) + { return (rates[token].getMedianValue(), numRates(token) == 0 ? 0 : FIXED1_UINT); } + /** + * @notice Returns the median of the currently stored rates for a specified rateFeedId. + * @dev Please note that this function respects the equivalentToken mapping, and so may + * return the median identified as an equivalent to the supplied rateFeedId. + * @param token The token for which the median value is being retrieved. + * @return uint256 The median exchange rate for rateFeedId (fixidity). + * @return uint256 denominator + */ + function medianRate(address token) external view returns (uint256, uint256) { + EquivalentToken storage equivalentToken = equivalentTokens[token]; + if (equivalentToken.token != address(0)) { + (uint256 equivalentMedianRate, uint256 denominator) = medianRateWithoutEquivalentMapping( + equivalentToken.token + ); + return (equivalentMedianRate, denominator); + } + + return medianRateWithoutEquivalentMapping(token); + } + /** * @notice Gets all elements from the doubly linked list. - * @param token The address of the token for which the CELO exchange rate is being reported. - * @return keys Keys of nn unpacked list of elements from largest to smallest. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the rates are being retrieved. + * @return keys Keys of an unpacked list of elements from largest to smallest. * @return values Values of an unpacked list of elements from largest to smallest. * @return relations Relations of an unpacked list of elements from largest to smallest. */ @@ -260,8 +372,10 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi /** * @notice Returns the number of timestamps. - * @param token The address of the token for which the CELO exchange rate is being reported. - * @return The number of oracle report timestamps for `token`. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the number of timestamps is being retrieved. + * @return uint256 The number of oracle report timestamps for the specified rateFeedId. */ function numTimestamps(address token) public view returns (uint256) { return timestamps[token].getNumElements(); @@ -269,8 +383,10 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi /** * @notice Returns the median timestamp. - * @param token The address of the token for which the CELO exchange rate is being reported. - * @return The median report timestamp for `token`. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the median timestamp is being retrieved. + * @return uint256 The median report timestamp for the specified rateFeedId. */ function medianTimestamp(address token) external view returns (uint256) { return timestamps[token].getMedianValue(); @@ -278,7 +394,9 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi /** * @notice Gets all elements from the doubly linked list. - * @param token The address of the token for which the CELO exchange rate is being reported. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the timestamps are being retrieved. * @return keys Keys of nn unpacked list of elements from largest to smallest. * @return values Values of an unpacked list of elements from largest to smallest. * @return relations Relations of an unpacked list of elements from largest to smallest. @@ -292,26 +410,33 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi } /** - * @notice Returns whether a report exists on token from oracle. - * @param token The address of the token for which the CELO exchange rate is being reported. + * @notice Checks if a report exists for a specified rateFeedId from a given oracle. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the report should be checked. * @param oracle The oracle whose report should be checked. + * @return bool True if a report exists, false otherwise. */ function reportExists(address token, address oracle) internal view returns (bool) { return rates[token].contains(oracle) && timestamps[token].contains(oracle); } /** - * @notice Returns the list of oracles for a particular token. - * @param token The address of the token whose oracles should be returned. - * @return The list of oracles for a particular token. + * @notice Returns the list of oracles for a speficied rateFeedId. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the oracles are being retrieved. + * @return address[] A list of oracles for the given rateFeedId. */ function getOracles(address token) external view returns (address[] memory) { return oracles[token]; } /** - * @notice Returns the expiry for the token if exists, if not the default. - * @param token The address of the token. + * @notice Returns the expiry for specified rateFeedId if it exists, if not the default is returned. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the report expiry is being retrieved. * @return The report expiry in seconds. */ function getTokenReportExpirySeconds(address token) public view returns (uint256) { @@ -324,7 +449,9 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi /** * @notice Removes an oracle value and updates the median. - * @param token The address of the token for which the CELO exchange rate is being reported. + * @dev Does not take the equivalentTokens mapping into account. + * For that, the underlying token should be queried. + * @param token The token for which the oracle report should be removed. * @param oracle The oracle whose value should be removed. * @dev This can be used to delete elements for oracles that have been removed. * However, a > 1 elements reports list should always be maintained @@ -338,6 +465,9 @@ contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initi uint256 newMedian = rates[token].getMedianValue(); if (newMedian != originalMedian) { emit MedianUpdated(token, newMedian); + if (address(breakerBox) != address(0)) { + breakerBox.checkAndSetBreakers(token); + } } } } diff --git a/packages/protocol/contracts/stability/interfaces/IBreakerBox.sol b/packages/protocol/contracts/stability/interfaces/IBreakerBox.sol new file mode 100644 index 00000000000..65322a6721b --- /dev/null +++ b/packages/protocol/contracts/stability/interfaces/IBreakerBox.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.5.13; + +/** + * @title Breaker Box Interface + * @notice Defines the basic interface for the Breaker Box + */ +interface IBreakerBox { + /** + * @dev Used to keep track of the status of a breaker for a specific rate feed. + * + * - TradingMode: Represents the trading mode the breaker is in for a rate feed. + * This uses a bitmask approach, meaning each bit represents a + * different trading mode. The final trading mode of the rate feed + * is obtained by applying a logical OR operation to the TradingMode + * of all breakers associated with that rate feed. This allows multiple + * breakers to contribute to the final trading mode simultaneously. + * Possible values: + * 0: bidirectional trading. + * 1: inflow only. + * 2: outflow only. + * 3: trading halted. + * + * - LastUpdatedTime: Records the last time the breaker status was updated. This is + * used to manage cooldown periods before the breaker can be reset. + * + * - Enabled: Indicates whether the breaker is enabled for the associated rate feed. + */ + struct BreakerStatus { + uint8 tradingMode; + uint64 lastUpdatedTime; + bool enabled; + } + + /** + * @notice Emitted when a new breaker is added to the breaker box. + * @param breaker The address of the breaker. + */ + event BreakerAdded(address indexed breaker); + + /** + * @notice Emitted when a breaker is removed from the breaker box. + * @param breaker The address of the breaker. + */ + event BreakerRemoved(address indexed breaker); + + /** + * @notice Emitted when a breaker is tripped by a rate feed. + * @param breaker The address of the breaker. + * @param rateFeedID The address of the rate feed. + */ + event BreakerTripped(address indexed breaker, address indexed rateFeedID); + + /** + * @notice Emitted when a new rate feed is added to the breaker box. + * @param rateFeedID The address of the rate feed. + */ + event RateFeedAdded(address indexed rateFeedID); + + /** + * @notice Emitted when dependencies for a rate feed are set. + * @param rateFeedID The address of the rate feed. + * @param dependencies The addresses of the dependendent rate feeds. + */ + event RateFeedDependenciesSet(address indexed rateFeedID, address[] indexed dependencies); + + /** + * @notice Emitted when a rate feed is removed from the breaker box. + * @param rateFeedID The address of the rate feed. + */ + event RateFeedRemoved(address indexed rateFeedID); + + /** + * @notice Emitted when the trading mode for a rate feed is updated + * @param rateFeedID The address of the rate feed. + * @param tradingMode The new trading mode. + */ + event TradingModeUpdated(address indexed rateFeedID, uint256 tradingMode); + + /** + * @notice Emitted after a reset attempt is successful. + * @param rateFeedID The address of the rate feed. + * @param breaker The address of the breaker. + */ + event ResetSuccessful(address indexed rateFeedID, address indexed breaker); + + /** + * @notice Emitted after a reset attempt fails when the + * rate feed fails the breakers reset criteria. + * @param rateFeedID The address of the rate feed. + * @param breaker The address of the breaker. + */ + event ResetAttemptCriteriaFail(address indexed rateFeedID, address indexed breaker); + + /** + * @notice Emitted after a reset attempt fails when cooldown time has not elapsed. + * @param rateFeedID The address of the rate feed. + * @param breaker The address of the breaker. + */ + event ResetAttemptNotCool(address indexed rateFeedID, address indexed breaker); + + /** + * @notice Emitted when the sortedOracles address is updated. + * @param newSortedOracles The address of the new sortedOracles. + */ + event SortedOraclesUpdated(address indexed newSortedOracles); + + /** + * @notice Emitted when the breaker is enabled or disabled for a rate feed. + * @param breaker The address of the breaker. + * @param rateFeedID The address of the rate feed. + * @param status Indicating the status. + */ + event BreakerStatusUpdated(address breaker, address rateFeedID, bool status); + + /** + * @notice Retrives an array of all breaker addresses. + */ + function getBreakers() external view returns (address[] memory); + + /** + * @notice Checks if a breaker with the specified address has been added to the breaker box. + * @param breaker The address of the breaker to check; + * @return A bool indicating whether or not the breaker has been added. + */ + function isBreaker(address breaker) external view returns (bool); + + /** + * @notice Checks breakers for the rateFeedID and sets correct trading mode + * if any breakers are tripped or need to be reset. + * @param rateFeedID The address of the rate feed to run checks for. + */ + function checkAndSetBreakers(address rateFeedID) external; + + /** + * @notice Gets the trading mode for the specified rateFeedID. + * @param rateFeedID The address of the rate feed to retrieve the trading mode for. + */ + function getRateFeedTradingMode(address rateFeedID) external view returns (uint8 tradingMode); +} diff --git a/packages/protocol/contracts/stability/test/MockSortedOracles.sol b/packages/protocol/contracts/stability/test/MockSortedOracles.sol index 6b35d2939bc..f3faeb0e55e 100644 --- a/packages/protocol/contracts/stability/test/MockSortedOracles.sol +++ b/packages/protocol/contracts/stability/test/MockSortedOracles.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.5.13; +pragma solidity >=0.5.13 <0.9.0; /** * @title A mock SortedOracles for testing. @@ -21,7 +21,7 @@ contract MockSortedOracles { function setMedianTimestampToNow(address token) external { // solhint-disable-next-line not-rely-on-time - medianTimestamp[token] = uint128(now); + medianTimestamp[token] = uint128(block.timestamp); } function setNumRates(address token, uint256 rate) external { diff --git a/packages/protocol/foundry.toml b/packages/protocol/foundry.toml index 5cb42a01093..42361f2ddee 100644 --- a/packages/protocol/foundry.toml +++ b/packages/protocol/foundry.toml @@ -4,15 +4,17 @@ out = 'out' test = 'test-sol' libs = ['lib', 'node_modules'] remappings = [ - 'ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/', - 'forge-std/=lib/celo-foundry/lib/forge-std/src/', - 'forge-std-8/=lib/celo-foundry-8/lib/forge-std/src/', - 'openzeppelin-solidity/=lib/openzeppelin-contracts/', - '@openzeppelin/contracts8/=lib/openzeppelin-contracts8/contracts/', - 'celo-foundry/=lib/celo-foundry/src/', - 'celo-foundry-8/=lib/celo-foundry-8/src/', - 'solidity-bytes-utils/=lib/solidity-bytes-utils/', - '@summa-tx/memview.sol/=lib/memview.sol', + 'openzeppelin-solidity/=lib/openzeppelin-contracts/', + 'solidity-bytes-utils/=lib/solidity-bytes-utils/', + 'forge-std/=lib/celo-foundry/lib/forge-std/src/', + 'ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/', + 'celo-foundry/=lib/celo-foundry/src/', + '@summa-tx/memview.sol/=lib/memview.sol', + 'celo-foundry-8/=lib/celo-foundry-8/src/', + 'forge-std-8/=lib/celo-foundry-8/lib/forge-std/src/', + '@celo-contracts-8=contracts-0.8/', + '@openzeppelin/contracts8/=lib/openzeppelin-contracts8/contracts/', + '@celo-contracts=contracts/' ] no_match_contract = "RandomTest" diff --git a/packages/protocol/lib/compatibility/ast-code.ts b/packages/protocol/lib/compatibility/ast-code.ts index 7c6bef1bf90..9c97633965b 100644 --- a/packages/protocol/lib/compatibility/ast-code.ts +++ b/packages/protocol/lib/compatibility/ast-code.ts @@ -45,7 +45,7 @@ const OUT_VOID_PARAMETER_STRING = 'void' * contract folders. */ export class ASTCodeCompatibilityReport { - constructor(private readonly changes: Change[]) {} + constructor(private readonly changes: Change[]) { } push(...changes: Change[]) { this.changes.push(...changes) } @@ -101,7 +101,7 @@ function createMethodIndex(methods: Method[]): MethodIndex { return Object.assign({}, ...asPairs) } -function mergeReports(reports: ASTCodeCompatibilityReport[]): ASTCodeCompatibilityReport{ +function mergeReports(reports: ASTCodeCompatibilityReport[]): ASTCodeCompatibilityReport { const report = new ASTCodeCompatibilityReport([]) reports.forEach((r: ASTCodeCompatibilityReport): void => { report.include(r) @@ -246,25 +246,31 @@ function generateASTCompatibilityReport(oldContract: ZContract, oldArtifacts: Bu /** * Runs an ast code comparison and returns the spotted changes from the built artifacts given. * - * @param oldArtifacts - * @param newArtifacts + * @param oldArtifactsSet + * @param newArtifactsSets */ export function reportASTIncompatibilities( // oldArtifacts also needs to be a set // https://github.com/celo-org/celo-monorepo/issues/10567 - oldArtifacts: BuildArtifacts, + oldArtifactsSet: BuildArtifacts[], newArtifactsSets: BuildArtifacts[]): ASTCodeCompatibilityReport { - + let out: ASTCodeCompatibilityReport[] = [] for (const newArtifacts of newArtifactsSets) { const reports = newArtifacts.listArtifacts() - .map((newArtifact) => { - const oldArtifact = oldArtifacts.getArtifactByName(newArtifact.contractName) - const oldZContract = oldArtifact ? makeZContract(oldArtifact) : null - return generateASTCompatibilityReport(oldZContract, oldArtifacts, makeZContract(newArtifact), newArtifacts) - }) - out = [...out, ...reports] - + .map((newArtifact) => { + + for (const oldArtifacts of oldArtifactsSet) { + const oldArtifact = oldArtifacts.getArtifactByName(newArtifact.contractName) + if (oldArtifact) { + return generateASTCompatibilityReport(makeZContract(oldArtifact), oldArtifacts, makeZContract(newArtifact), newArtifacts) + } + } + + return generateASTCompatibilityReport(null, oldArtifactsSet[0], makeZContract(newArtifact), newArtifacts) + }) + out = [...out, ...reports] + } return mergeReports(out) diff --git a/packages/protocol/lib/compatibility/ast-layout.ts b/packages/protocol/lib/compatibility/ast-layout.ts index 28cc3dee8d6..c9c79205945 100644 --- a/packages/protocol/lib/compatibility/ast-layout.ts +++ b/packages/protocol/lib/compatibility/ast-layout.ts @@ -8,7 +8,7 @@ import { compareStorageLayouts, getStorageLayout } from '@openzeppelin/upgrades'; -const Web3 = require('web3') +const Web3 = require('web3') const web3 = new Web3(null) @@ -42,7 +42,7 @@ export const getLayout = (artifact: Artifact, artifacts: BuildArtifacts) => { const selectIncompatibleOperations = (diff: Operation[]) => diff.filter(operation => operation.action !== 'append' - && !(operation.action === 'rename' && (`deprecated_${operation.original.label}` === operation.updated.label && operation.original.type === operation.updated.type))) + && !(operation.action === 'rename' && (`deprecated_${operation.original.label}` === operation.updated.label && operation.original.type === operation.updated.type))) export interface ASTStorageCompatibilityReport { contract: string @@ -117,7 +117,7 @@ const compareStructDefinitions = (oldType: TypeInfo, newType: TypeInfo, structEx if (oldMember.label !== newMember.label && `deprecated_${oldMember.label}` !== newMember.label) { return `struct ${newType.label} had ${oldMember.label} in slot ${i}, now has ${newMember.label}` } - + if (oldMember.type !== newMember.type) { return `struct ${newType.label}'s member ${newMember.label} changed type from ${oldMember.type} to ${newMember.type}` } @@ -164,7 +164,7 @@ const isStructExpandable = (oldType: TypeInfo, oldLayout: StorageLayoutInfo) => return !oldLayout.storage.some(storage => storage.type === structString) } -const generateStructsCompatibilityReport = (oldLayout: StorageLayoutInfo, newLayout: StorageLayoutInfo): {compatible: boolean, errors: any[], expanded?: boolean} => { +const generateStructsCompatibilityReport = (oldLayout: StorageLayoutInfo, newLayout: StorageLayoutInfo): { compatible: boolean, errors: any[], expanded?: boolean } => { let compatible = true let errors = [] let expanded: boolean @@ -191,40 +191,43 @@ const generateStructsCompatibilityReport = (oldLayout: StorageLayoutInfo, newLay } } -export const generateCompatibilityReport = (oldArtifact: Artifact, oldArtifacts: BuildArtifacts, - newArtifact: Artifact, newArtifacts: BuildArtifacts) - : {contract: string, compatible: boolean, errors: any[], expanded?: boolean } => { - const oldLayout = getLayout(oldArtifact, oldArtifacts) - const newLayout = getLayout(newArtifact, newArtifacts) - const layoutReport = generateLayoutCompatibilityReport(oldLayout, newLayout) - const structsReport = generateStructsCompatibilityReport(oldLayout, newLayout) +export const generateCompatibilityReport = (oldArtifact: Artifact, oldArtifacts: BuildArtifacts, + newArtifact: Artifact, newArtifacts: BuildArtifacts) + : { contract: string, compatible: boolean, errors: any[], expanded?: boolean } => { + const oldLayout = getLayout(oldArtifact, oldArtifacts) + const newLayout = getLayout(newArtifact, newArtifacts) + const layoutReport = generateLayoutCompatibilityReport(oldLayout, newLayout) + const structsReport = generateStructsCompatibilityReport(oldLayout, newLayout) - return { - contract: newArtifact.contractName, - compatible: layoutReport.compatible && structsReport.compatible, - errors: layoutReport.errors.concat(structsReport.errors), - expanded: structsReport.expanded - } + return { + contract: newArtifact.contractName, + compatible: layoutReport.compatible && structsReport.compatible, + errors: layoutReport.errors.concat(structsReport.errors), + expanded: structsReport.expanded + } } -export const reportLayoutIncompatibilities = (oldArtifacts: BuildArtifacts, newArtifactsSets: BuildArtifacts[]): ASTStorageCompatibilityReport[] => { +export const reportLayoutIncompatibilities = (oldArtifactsSet: BuildArtifacts[], newArtifactsSets: BuildArtifacts[]): ASTStorageCompatibilityReport[] => { let out: ASTStorageCompatibilityReport[] = [] for (const newArtifacts of newArtifactsSets) { const reports = newArtifacts.listArtifacts().map((newArtifact) => { - const oldArtifact = oldArtifacts.getArtifactByName(newArtifact.contractName) - if (oldArtifact !== undefined) { - return generateCompatibilityReport(oldArtifact, oldArtifacts, newArtifact, newArtifacts) - } else { - // Generate an empty report for new contracts, which are, by definition, backwards - // compatible. - return { - contract: newArtifact.contractName, - compatible: true, - errors: [] + + for (const oldArtifacts of oldArtifactsSet) { + const oldArtifact = oldArtifacts.getArtifactByName(newArtifact.contractName) + if (oldArtifact !== undefined) { + return generateCompatibilityReport(oldArtifact, oldArtifacts, newArtifact, newArtifacts) } } + + // Generate an empty report for new contracts, which are, by definition, backwards + // compatible. + return { + contract: newArtifact.contractName, + compatible: true, + errors: [] + } }) - + out = [...out, ...reports] } return out diff --git a/packages/protocol/lib/compatibility/ast-version.ts b/packages/protocol/lib/compatibility/ast-version.ts index 6adfb4c8055..30b77d51fbc 100644 --- a/packages/protocol/lib/compatibility/ast-version.ts +++ b/packages/protocol/lib/compatibility/ast-version.ts @@ -11,10 +11,10 @@ const abi = require('ethereumjs-abi') * A mapping {contract name => {@link ContractVersion}}. */ export class ASTContractVersions { - static fromArtifacts = async (artifactsSet: BuildArtifacts[]): Promise=> { + static fromArtifacts = async (artifactsSet: BuildArtifacts[]): Promise => { const contracts = {} - for (const artifacts of artifactsSet){ + for (const artifacts of artifactsSet) { await Promise.all(artifacts.listArtifacts().filter(c => !isLibrary(c.contractName, [artifacts])).map(async (artifact) => { contracts[artifact.contractName] = await getContractVersion(artifact) })) @@ -23,7 +23,7 @@ export class ASTContractVersions { return new ASTContractVersions(contracts) } - constructor(public readonly contracts: ContractVersionIndex) {} + constructor(public readonly contracts: ContractVersionIndex) { } } /** @@ -40,7 +40,7 @@ export async function getContractVersion(artifact: Artifact): Promise => { - const oldVersions = await ASTContractVersions.fromArtifacts([oldArtifacts]) + static create = async (oldArtifactsSet: BuildArtifacts[], newArtifactsSet: BuildArtifacts[], expectedVersionDeltas: ContractVersionDeltaIndex): Promise => { + const oldVersions = await ASTContractVersions.fromArtifacts(oldArtifactsSet) const newVersions = await ASTContractVersions.fromArtifacts(newArtifactsSet) const contracts = {} - Object.keys(newVersions.contracts).map((contract:string) => { + Object.keys(newVersions.contracts).map((contract: string) => { const versionDelta = expectedVersionDeltas[contract] === undefined ? ContractVersionDelta.fromChanges(false, false, false, false) : expectedVersionDeltas[contract] const oldVersion = oldVersions.contracts[contract] === undefined ? null : oldVersions.contracts[contract] contracts[contract] = new ContractVersionChecker(oldVersion, newVersions.contracts[contract], versionDelta) @@ -69,7 +69,7 @@ export class ASTContractVersionsChecker { return new ASTContractVersionsChecker(contracts) } - constructor(public readonly contracts: ContractVersionCheckerIndex) {} + constructor(public readonly contracts: ContractVersionCheckerIndex) { } /** * @return a new {@link ASTContractVersionsChecker} with the same contracts @@ -90,7 +90,7 @@ export class ASTContractVersionsChecker { } - public mismatches = () : ASTContractVersionsChecker => { + public mismatches = (): ASTContractVersionsChecker => { const mismatches = {} Object.keys(this.contracts).map((contract: string) => { if (!this.contracts[contract].matches()) { diff --git a/packages/protocol/lib/compatibility/ignored-contracts-v9.ts b/packages/protocol/lib/compatibility/ignored-contracts-v9.ts index 6bef5c836d9..5b0f6b790d0 100644 --- a/packages/protocol/lib/compatibility/ignored-contracts-v9.ts +++ b/packages/protocol/lib/compatibility/ignored-contracts-v9.ts @@ -10,19 +10,18 @@ export const ignoredContractsV9 = [ 'StableTokenRegistry', 'Reserve', 'ReserveSpenderMultiSig', +] - 'SortedOracles' - // Note: Sorted Oracles is a Celo Core Contract - // but as it has also been modified and deployed but the Mento team. - // We currently need work to be able to upgrade it again: - // https://github.com/celo-org/celo-monorepo/issues/10435 +export const ignoredContractsV9Only = [ + 'SortedOracles' + // Between CR9 and CR10, a Mento upgrade MU03 also upgraded SortedOracles. For the purposes of our compatibility tests, we use the Mento version of the contract in CR10, so that we're comparing the most recent pre-CR10 contracts with the CR10 versions. ] export function getReleaseVersion(tag: string) { const regexp = /core-contracts.v(?.*[0-9])/gm const matches = regexp.exec(tag) const version = parseInt(matches?.groups?.version ?? '0', 10) - if ((version) == 0){ + if ((version) == 0) { throw `Tag doesn't have the correct format ${tag}` } return version diff --git a/packages/protocol/lib/compatibility/utils.ts b/packages/protocol/lib/compatibility/utils.ts index 5482a3117eb..e051619c25f 100644 --- a/packages/protocol/lib/compatibility/utils.ts +++ b/packages/protocol/lib/compatibility/utils.ts @@ -18,7 +18,7 @@ export class ASTBackwardReport { static create = ( oldArtifactsFolder: string, newArtifactsFolders: string[], - oldArtifacts: BuildArtifacts, + oldArtifacts: BuildArtifacts[], newArtifacts: BuildArtifacts[], exclude: RegExp, categorizer: Categorizer, @@ -55,7 +55,7 @@ export class ASTBackwardReport { public readonly newArtifactsFolder: string[], public readonly exclude: string, public readonly report: ASTDetailedVersionedReport - ) {} + ) { } } function ensureValidArtifacts(artifactsPaths: string[]): void { diff --git a/packages/protocol/lib/compatibility/verify-bytecode.ts b/packages/protocol/lib/compatibility/verify-bytecode.ts index 96abf763e2c..a7b1f5af13b 100644 --- a/packages/protocol/lib/compatibility/verify-bytecode.ts +++ b/packages/protocol/lib/compatibility/verify-bytecode.ts @@ -1,18 +1,18 @@ /* eslint-disable no-console: 0 */ import { ensureLeading0x } from '@celo/base/lib/address' import { - LibraryAddresses, - LibraryPositions, - linkLibraries, - stripMetadata, - verifyAndStripLibraryPrefix, + LibraryAddresses, + LibraryPositions, + linkLibraries, + stripMetadata, + verifyAndStripLibraryPrefix, } from '@celo/protocol/lib/bytecode' import { verifyProxyStorageProof } from '@celo/protocol/lib/proxy-utils' import { ProposalTx } from '@celo/protocol/scripts/truffle/make-release' import { BuildArtifacts } from '@openzeppelin/upgrades' import { ProxyInstance, RegistryInstance } from 'types' import Web3 from 'web3' -import { ignoredContractsV9 } from './ignored-contracts-v9' +import { ignoredContractsV9, ignoredContractsV9Only } from './ignored-contracts-v9' let ignoredContracts = [ // This contract is not proxied @@ -143,8 +143,7 @@ const dfsStep = async (queue: string[], visited: Set, context: Verificat throw new Error(`${contract}'s onchain and compiled bytecodes do not match`) } else { console.log( - `${ - isLibrary(contract, context) ? 'Library' : 'Contract' + `${isLibrary(contract, context) ? 'Library' : 'Contract' } deployed at ${implementationAddress} matches ${contract}` ) } @@ -231,15 +230,17 @@ export const verifyBytecodes = async ( const compiledContracts = artifacts.listArtifacts().map((a) => a.contractName) - if (version >= 9) { + if (version > 9) { ignoredContracts = [...ignoredContracts, ...ignoredContractsV9] + } else if (version == 9) { + ignoredContracts = [...ignoredContracts, ...ignoredContractsV9, ...ignoredContractsV9Only] } const queue = contracts.filter( (contract) => !ignoredContracts.includes(contract) - ).filter( - (contract) => compiledContracts.includes(contract) - ) + ).filter( + (contract) => compiledContracts.includes(contract) + ) const visited: Set = new Set(queue) diff --git a/packages/protocol/scripts/bash/check-versions.sh b/packages/protocol/scripts/bash/check-versions.sh index e001a50045c..789c77af8f3 100755 --- a/packages/protocol/scripts/bash/check-versions.sh +++ b/packages/protocol/scripts/bash/check-versions.sh @@ -52,6 +52,7 @@ yarn ts-node scripts/check-backward.ts sem_check \ --old_contracts $BRANCH_BUILD_DIR/contracts \ --new_contracts $NEW_BRANCH_BUILD_DIR/contracts \ --exclude $CONTRACT_EXCLUSION_REGEX \ + --new_branch $NEW_BRANCH \ $REPORT_FLAG git checkout $CURRENT_HASH -- migrationsConfig.js diff --git a/packages/protocol/scripts/bash/contract-exclusion-regex.sh b/packages/protocol/scripts/bash/contract-exclusion-regex.sh index 27a44b06aed..4ffeb3400dd 100644 --- a/packages/protocol/scripts/bash/contract-exclusion-regex.sh +++ b/packages/protocol/scripts/bash/contract-exclusion-regex.sh @@ -18,17 +18,8 @@ if [ $VERSION_NUMBER -gt 8 ] CONTRACT_EXCLUSION_REGEX="$CONTRACT_EXCLUSION_REGEX|^Ownable|Initializable|BLS12_377Passthrough|BLS12_381Passthrough]UniswapV2ERC20" fi - # https://github.com/celo-org/celo-monorepo/issues/10435 - # SortedOracles is currently not deployable - # after fixing that this should be modified to VERSION_NUMBER==10 -if [ $VERSION_NUMBER -gt 9 ] +# In CR9 the SortedOracles contract was deployed by Mento team, in CR10 we redeployed it ourselves +if [ $VERSION_NUMBER -eq 9 ] then CONTRACT_EXCLUSION_REGEX="$CONTRACT_EXCLUSION_REGEX|SortedOracles" fi - -# TODO remove this after merge by fixing the report creation scipt to include GasPriceMinimum (0.8 contracts) -# https://github.com/celo-org/celo-monorepo/issues/10567 -if [ $VERSION_NUMBER -gt 9 ] - then - CONTRACT_EXCLUSION_REGEX="$CONTRACT_EXCLUSION_REGEX|GasPriceMinimum" -fi diff --git a/packages/protocol/scripts/bash/verify-deployed.sh b/packages/protocol/scripts/bash/verify-deployed.sh index bcb0aaba69a..b358b0ff103 100755 --- a/packages/protocol/scripts/bash/verify-deployed.sh +++ b/packages/protocol/scripts/bash/verify-deployed.sh @@ -32,4 +32,31 @@ done source scripts/bash/release-lib.sh build_tag $BRANCH $LOG_FILE +if [ "$BRANCH" = "core-contracts.v10" ]; then + if [ ! -d "build/mento" ]; then + mkdir -p build/mento + cd build/mento + git clone --depth 1 --branch v2.2.1 https://github.com/mento-protocol/mento-core.git + cd mento-core + yarn + forge install + forge build + else + cd build/mento/mento-core + fi + + # Replace the bytecode of the SortedOracles contracts with the bytecode from Mento core v2.2.1 + orig_value_SortedOracles=$(jq -r '.deployedBytecode.object' out/SortedOracles.sol/SortedOracles.json) + substring_to_replace='__$e9f4a9f9de32ce6d7070252f1b707ecbd2$__' # Foundry artifact bytecode differs for linked libraries, instead of library name it inserts a hashed value of library name in-place + replacement='__AddressSortedLinkedListWithMedian_____' # Replace with Truffle specific library placeholder + value_SortedOracles="${orig_value_SortedOracles//$substring_to_replace/$replacement}" + jq --arg value "$value_SortedOracles" '.deployedBytecode = $value' ../../../$BUILD_DIR/contracts/SortedOracles.json > "temp.json" && mv "temp.json" ../../../$BUILD_DIR/contracts/SortedOracles.json + + # Replace the bytecode of the AddressSortedLinkedListWithMedian contract with the bytecode from Mento core v2.2.1 + value_AddressSortedLinkedListWithMedian=$(jq -r '.deployedBytecode.object' out/AddressSortedLinkedListWithMedian.sol/AddressSortedLinkedListWithMedian.json) + jq --arg value "$value_AddressSortedLinkedListWithMedian" '.deployedBytecode = $value' ../../../$BUILD_DIR/contracts/AddressSortedLinkedListWithMedian.json > temp.json && mv temp.json ../../../$BUILD_DIR/contracts/AddressSortedLinkedListWithMedian.json + + cd ../../../ +fi + yarn run truffle exec ./scripts/truffle/verify-bytecode.js --network $NETWORK --build_artifacts $BUILD_DIR/contracts --branch $BRANCH --librariesFile "libraries.json" $FORNO diff --git a/packages/protocol/scripts/check-backward.ts b/packages/protocol/scripts/check-backward.ts index 311377fa978..1c77ca098ed 100644 --- a/packages/protocol/scripts/check-backward.ts +++ b/packages/protocol/scripts/check-backward.ts @@ -1,5 +1,7 @@ import { ASTContractVersionsChecker } from '@celo/protocol/lib/compatibility/ast-version' import { DefaultCategorizer } from '@celo/protocol/lib/compatibility/categorizer' +import { getReleaseVersion } from '@celo/protocol/lib/compatibility/ignored-contracts-v9' +import { CategorizedChanges } from '@celo/protocol/lib/compatibility/report' import { ASTBackwardReport, instantiateArtifacts } from '@celo/protocol/lib/compatibility/utils' import { writeJsonSync } from 'fs-extra' import path from 'path' @@ -43,6 +45,11 @@ const argv = yargs default: false, type: 'boolean', }) + .option('new_branch', { + alias: 'b', + description: 'Branch name (for versioning)', + type: 'string', + }) .help() .alias('help', 'h') .showHelpOnFail(true) @@ -51,6 +58,7 @@ const argv = yargs // old artifacts folder needs to be generalized https://github.com/celo-org/celo-monorepo/issues/10567 const oldArtifactsFolder = path.relative(process.cwd(), argv.old_contracts) +const oldArtifactsFolder08 = path.relative(process.cwd(), argv.old_contracts + '-0.8') const newArtifactsFolder = path.relative(process.cwd(), argv.new_contracts) const newArtifactsFolder08 = path.relative(process.cwd(), argv.new_contracts + '-0.8') const newArtifactsFolders = [newArtifactsFolder, newArtifactsFolder08] @@ -65,6 +73,7 @@ const outFile = argv.output_file ? argv.output_file : tmp.tmpNameSync({}) const exclude: RegExp = argv.exclude ? new RegExp(argv.exclude) : null // old artifacts needs to be generalized https://github.com/celo-org/celo-monorepo/issues/10567 const oldArtifacts = instantiateArtifacts(oldArtifactsFolder) +const oldArtifacts08 = instantiateArtifacts(oldArtifactsFolder08) const newArtifacts = instantiateArtifacts(newArtifactsFolder) const newArtifacts08 = instantiateArtifacts(newArtifactsFolder08) @@ -72,13 +81,24 @@ try { const backward = ASTBackwardReport.create( oldArtifactsFolder, newArtifactsFolders, - oldArtifacts, + [oldArtifacts, oldArtifacts08], [newArtifacts, newArtifacts08], exclude, new DefaultCategorizer(), out ) + try { + const version = getReleaseVersion(argv.new_branch) + if (version === 11) { + // force redeploy of AddressSortedLinkedListWithMedian for CR11 + // since it was deployed by Mento team with different settings and bytecode + backward.report.libraries.AddressSortedLinkedListWithMedian = {} as CategorizedChanges + } + } catch (error) { + out(`Error parsing branch name: ${argv.new_branch}\n`) + } + out(`Writing compatibility report to ${outFile} ...`) writeJsonSync(outFile, backward, { spaces: 2 }) out('Done\n') @@ -88,7 +108,7 @@ try { } else if (argv._.includes(COMMAND_SEM_CHECK)) { const doVersionCheck = async () => { const versionChecker = await ASTContractVersionsChecker.create( - oldArtifacts, + [oldArtifacts, oldArtifacts08], [newArtifacts, newArtifacts08], backward.report.versionDeltas() ) diff --git a/packages/protocol/test-sol/common/GasPriceMinimum.t.sol b/packages/protocol/test-sol/common/GasPriceMinimum.t.sol index 137c9f042b9..8f1a9e9adb8 100644 --- a/packages/protocol/test-sol/common/GasPriceMinimum.t.sol +++ b/packages/protocol/test-sol/common/GasPriceMinimum.t.sol @@ -6,6 +6,8 @@ import "celo-foundry-8/Test.sol"; import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/stability/interfaces/ISortedOracles.sol"; +import "@celo-contracts/stability/test/MockSortedOracles.sol"; import "@celo-contracts-8/common/GasPriceMinimum.sol"; @@ -14,8 +16,10 @@ contract GasPriceMinimumTest is Test { IRegistry registry; GasPriceMinimum public gasPriceMinimum; + MockSortedOracles sortedOracles; address owner; address nonOwner; + address celoToken; uint256 gasPriceMinimumFloor = 100; uint256 initialGasPriceMinimum = gasPriceMinimumFloor; @@ -34,13 +38,21 @@ contract GasPriceMinimumTest is Test { function setUp() public virtual { owner = address(this); nonOwner = actor("nonOwner"); + celoToken = actor("CeloToken"); deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + + // deployCodeTo("SortedOracles.sol", abi.encode(true), sortedOracleAddress); + // fails with `data did not match any variant of untagged enum Bytecode at line 822 column 3]` + sortedOracles = new MockSortedOracles(); + gasPriceMinimum = new GasPriceMinimum(true); registry = IRegistry(registryAddress); registry.setAddressFor("GasPriceMinimum", address(gasPriceMinimum)); + registry.setAddressFor("SortedOracles", address(sortedOracles)); + registry.setAddressFor("GoldToken", celoToken); gasPriceMinimum.initialize( registryAddress, @@ -57,10 +69,6 @@ contract GasPriceMinimumTest_initialize is GasPriceMinimumTest { assertEq(gasPriceMinimum.owner(), owner); } - function test_shouldSetTheGasPriceMinimum() public { - assertEq(gasPriceMinimum.getGasPriceMinimum(address(0)), initialGasPriceMinimum); - } - function test_shouldHaveTargetDensity() public { assertEq(gasPriceMinimum.targetDensity(), targetDensity); } diff --git a/packages/protocol/test-sol/governance/network/Governance.t.sol b/packages/protocol/test-sol/governance/network/Governance.t.sol index 90144a8045a..d7694ebb6ae 100644 --- a/packages/protocol/test-sol/governance/network/Governance.t.sol +++ b/packages/protocol/test-sol/governance/network/Governance.t.sol @@ -42,12 +42,16 @@ contract GovernanceMock is Governance(true) { _removeVotesWhenRevokingDelegatedVotes(account, maxAmountAllowed); } - function setDeprecatedWeight(address voterAddress, uint256 proposalIndex, uint256 weight) - external - { + function setDeprecatedWeight( + address voterAddress, + uint256 proposalIndex, + uint256 weight, + uint256 proposalId + ) external { Voter storage voter = voters[voterAddress]; VoteRecord storage voteRecord = voter.referendumVotes[proposalIndex]; voteRecord.deprecated_weight = weight; + voteRecord.proposalId = proposalId; } } @@ -3476,7 +3480,7 @@ contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { vm.warp(block.timestamp + governance.dequeueFrequency()); vm.prank(accApprover); governance.approve(proposalId, 0); - governance.setDeprecatedWeight(accVoter, 0, 100); + governance.setDeprecatedWeight(accVoter, 0, 100, 1); assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 100); } @@ -3505,6 +3509,27 @@ contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 0); } + function test_return0Votes_WhenIndexOfProposalGetsReused() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.votePartially(proposalId, 0, 10, 30, 0); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + governance.execute(proposalId, 0); + vm.warp(block.timestamp + governance.getExecutionStageDuration() + 1); + assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 0); + + governance.dequeueProposalsIfReady(); + proposalId = makeValidProposal(); + + vm.warp(block.timestamp + governance.dequeueFrequency() + 1); + vm.prank(accApprover); + governance.approve(proposalId, 0); + assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 0); + } + function test_returnFullWeightWhenUpvoting_WhenProposalInQueue() public { vm.prank(accOwner); governance.setConcurrentProposals(3); diff --git a/packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol b/packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol new file mode 100644 index 00000000000..1cc122a8c0b --- /dev/null +++ b/packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: apache-2.0 +pragma solidity >=0.8.7 <=0.8.20; + +import "celo-foundry-8/Test.sol"; + +import "../../contracts/common/FixidityLib.sol"; +import "../../contracts/common/interfaces/IRegistry.sol"; + +// Contract to test +import "@celo-contracts-8/stability/CeloFeeCurrencyAdapterOwnable.sol"; +import "@celo-contracts-8/stability/interfaces/IFeeCurrency.sol"; +import "@openzeppelin/contracts8/token/ERC20/ERC20.sol"; +import "forge-std/console.sol"; + +contract FeeCurrency6DecimalsTest is ERC20, IFeeCurrency { + uint256 debited; + + constructor(uint256 initialSupply) ERC20("ExampleFeeCurrency", "EFC") { + _mint(msg.sender, initialSupply); + } + + function debitGasFees(address from, uint256 value) external { + _burn(from, value); + debited = value; + } + + // New function signature, will be used when all fee currencies have migrated + function creditGasFees(address[] calldata recipients, uint256[] calldata amounts) public { + require(recipients.length == amounts.length, "Recipients and amounts must be the same length."); + + uint256 totalSum = 0; + + for (uint256 i = 0; i < recipients.length; i++) { + _mint(recipients[i], amounts[i]); + totalSum += amounts[i]; + } + + require(debited == totalSum, "Cannot credit more than debited."); + debited = 0; + } + + // Old function signature for backwards compatibility + function creditGasFees( + address from, + address feeRecipient, + address, // gatewayFeeRecipient, unused + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256, // gatewayFee, unused + uint256 baseTxFee + ) public { + require(debited == refund + tipTxFee + baseTxFee, "Cannot credit more than debited."); + // Calling the new creditGasFees would make sense here, but that is not + // possible due to its calldata arguments. + _mint(from, refund); + _mint(feeRecipient, tipTxFee); + _mint(communityFund, baseTxFee); + + debited = 0; + } + + function decimals() public pure override returns (uint8) { + return 6; + } +} + +contract CeloFeeCurrencyAdapterTestContract is CeloFeeCurrencyAdapterOwnable { + constructor(bool test) CeloFeeCurrencyAdapterOwnable(test) {} + + function upscaleVisible(uint256 value) external view returns (uint256) { + return upscale(value); + } + + function downscaleVisible(uint256 value) external view returns (uint256) { + return downscale(value); + } +} + +contract FeeCurrencyAdapterTest is Test { + using FixidityLib for FixidityLib.Fraction; + + event GasFeesDebited(address indexed debitedFrom, uint256 debitedAmount); + + event GasFeesCredited( + address indexed refundRecipient, + address indexed tipRecipient, + address indexed baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256 baseFeeAmount + ); + + CeloFeeCurrencyAdapterTestContract public feeCurrencyAdapter; + CeloFeeCurrencyAdapterTestContract public feeCurrencyAdapterForFuzzyTests; + address owner; + address nonOwner; + IFeeCurrency feeCurrency; + + uint256 initialSupply = 10_000; + + function setUp() public virtual { + owner = address(this); + nonOwner = actor("nonOwner"); + + feeCurrencyAdapter = new CeloFeeCurrencyAdapterTestContract(true); + feeCurrencyAdapterForFuzzyTests = new CeloFeeCurrencyAdapterTestContract(true); + + address feeCurrencyAddress = actor("feeCurrency"); + + string memory name = "tokenName"; + string memory symbol = "tN"; + + feeCurrency = new FeeCurrency6DecimalsTest(initialSupply); + + feeCurrencyAdapter.initialize(address(feeCurrency), "wrapper", "wr", 18); + } +} + +contract FeeCurrencyAdapter_Initialize is FeeCurrencyAdapterTest { + function test_ShouldSetDigitDifference() public { + assertEq(feeCurrencyAdapter.digitDifference(), 10**12); + } + + function test_shouldRevertWhenCalledAgain() public { + vm.expectRevert("contract already initialized"); + feeCurrencyAdapter.initialize(address(feeCurrency), "adapter", "ad", 18); + } + + function test_ShouldSucceed_WhenExpectedDecimalsAreMoreThenDecimals_Fuzz(uint8 amount) public { + vm.assume(amount > 6); + vm.assume(amount < 50); + console.log("amount", amount); + feeCurrencyAdapterForFuzzyTests.initialize(address(feeCurrency), "adapter", "ad", amount); + } + + function test_ShouldRevert_WhenExpectedDecimalsAreLessThenDecimals() public { + vm.expectRevert("Decimals of adapted token must be < expected decimals."); + feeCurrencyAdapterForFuzzyTests.initialize(address(feeCurrency), "adapter", "ad", 5); + } + + function test_ShouldRevert_WhenExpectedDecimalsAreEqualToDecimals() public { + vm.expectRevert("Decimals of adapted token must be < expected decimals."); + feeCurrencyAdapterForFuzzyTests.initialize(address(feeCurrency), "adapter", "ad", 6); + } +} + +contract FeeCurrencyAdapter_BalanceOf is FeeCurrencyAdapterTest { + function test_shouldReturnBalanceOf() public { + assertEq(feeCurrency.balanceOf(address(this)), initialSupply); + assertEq(feeCurrencyAdapter.balanceOf(address(this)), initialSupply * 1e12); + } +} + +contract FeeCurrencyAdapter_TotalSupply is FeeCurrencyAdapterTest { + function test_shouldReturnTotalSupply() public { + assertEq(feeCurrency.totalSupply(), initialSupply); + assertEq(feeCurrencyAdapter.totalSupply(), initialSupply * 1e12); + } +} + +contract FeeCurrencyAdapter_Decimals is FeeCurrencyAdapterTest { + function test_shouldReturnDecimals() public { + assertEq(feeCurrencyAdapter.decimals(), 18); + } +} + +contract FeeCurrencyAdapter_DebitGasFees is FeeCurrencyAdapterTest { + function test_shouldDebitGasFees() public { + uint256 amount = 1000 * 1e12; + vm.prank(address(0)); + feeCurrencyAdapter.debitGasFees(address(this), amount); + assertEq(feeCurrency.balanceOf(address(this)), initialSupply - amount / 1e12); + assertEq(feeCurrencyAdapter.balanceOf(address(this)), (initialSupply * 1e12 - amount)); + assertEq(feeCurrencyAdapter.debited(), amount / 1e12); + } + + function test_shouldRevert_WhenNotCalledByVm() public { + vm.expectRevert("Only VM can call"); + feeCurrencyAdapter.debitGasFees(address(this), 1000); + } + + function test_ShouldRevert_WhenScaledDebitValueIs0() public { + vm.expectRevert("Scaled debit value must be > 0."); + vm.prank(address(0)); + feeCurrencyAdapter.debitGasFees(address(this), 0); + } + + function test_ShouldDebitCorrectAmount_WhenExpectedDigitsOnlyOneBigger() public { + debitFuzzyHelper(7, 1e1); + } + + function test_ShouldDebitCorrectAmount_WhenExpectedDigitsBigger() public { + debitFuzzyHelper(10, 1e4); + } + + function test_ShouldDebitCorrectAmount_WhenExpectedDigitsALotBigger() public { + debitFuzzyHelper(30, 1e24); + } + + function debitFuzzyHelper(uint8 expectedDigits, uint256 multiplier) public { + feeCurrencyAdapterForFuzzyTests.initialize( + address(feeCurrency), + "adapter", + "ad", + expectedDigits + ); + uint256 amount = 1000 * multiplier; + vm.prank(address(0)); + feeCurrencyAdapterForFuzzyTests.debitGasFees(address(this), amount); + assertEq(feeCurrency.balanceOf(address(this)), initialSupply - amount / multiplier); + assertEq( + feeCurrencyAdapterForFuzzyTests.balanceOf(address(this)), + (initialSupply * multiplier - amount) + ); + assertEq(feeCurrencyAdapterForFuzzyTests.debited(), amount / multiplier); + } +} + +contract FeeCurrencyAdapter_CreditGasFees is FeeCurrencyAdapterTest { + function test_shouldCreditGasFees() public { + uint256 amount = 1000 * 1e12; + vm.prank(address(0)); + feeCurrencyAdapter.debitGasFees(address(this), amount); + + vm.prank(address(0)); + feeCurrencyAdapter.creditGasFees( + address(this), + address(this), + address(0), + address(this), + amount / 4, + amount / 4, + 0, + amount / 4 + ); + assertEq(feeCurrency.balanceOf(address(this)), initialSupply); + assertEq(feeCurrencyAdapter.balanceOf(address(this)), initialSupply * 1e12); + } + + function test_shouldRevert_WhenTryingToCreditMoreThanBurned() public { + uint256 amount = 1 * 1e12; + vm.prank(address(0)); + feeCurrencyAdapter.debitGasFees(address(this), amount); + + vm.expectRevert("Cannot credit more than debited."); + vm.prank(address(0)); + feeCurrencyAdapter.creditGasFees( + address(this), + address(this), + address(this), + address(this), + 1 ether, + 1 ether, + 1 ether, + 1 ether + ); + } + + function test_shouldRevert_WhenNotCalledByVm() public { + vm.expectRevert("Only VM can call"); + feeCurrencyAdapter.creditGasFees( + address(this), + address(this), + address(this), + address(this), + 1000, + 1000, + 1000, + 1000 + ); + } + + function test_shouldNotRunFunctionBody_WhenDebitedIs0() public { + uint256 balanceBefore = feeCurrency.balanceOf(address(this)); + vm.prank(address(0)); + feeCurrencyAdapter.creditGasFees( + address(this), + address(this), + address(this), + address(this), + 1000, + 1000, + 1000, + 1000 + ); + uint256 balanceAfter = feeCurrency.balanceOf(address(this)); + assertEq(balanceBefore, balanceAfter); + } + + function test_shouldCreditGasFees_WhenOnlyOneBigger() public { + creditFuzzHelper(7, 1e1); + } + + function test_shouldCreditGasFees_WhenBigger() public { + creditFuzzHelper(10, 1e4); + } + + function test_shouldCreditGasFees_WhenALotBigger() public { + creditFuzzHelper(30, 1e24); + } + + function creditFuzzHelper(uint8 expectedDigits, uint256 multiplier) public { + uint256 originalAmount = 1000; + uint256 amount = originalAmount * multiplier; + console.log("amount", amount); + + address secondAddress = actor("secondAddress"); + address thirdAddress = actor("thirdAddress"); + + feeCurrencyAdapterForFuzzyTests.initialize( + address(feeCurrency), + "adapter", + "ad", + expectedDigits + ); + vm.prank(address(0)); + feeCurrencyAdapterForFuzzyTests.debitGasFees(address(this), amount); + + vm.prank(address(0)); + feeCurrencyAdapterForFuzzyTests.creditGasFees( + address(this), + secondAddress, + address(0), + thirdAddress, + amount / 4, + amount / 4, + 0, + amount / 4 + ); + assertEq( + feeCurrency.balanceOf(address(this)), + (initialSupply - originalAmount) + (originalAmount / 4) + ); + assertEq(feeCurrency.balanceOf(secondAddress), originalAmount / 4); + assertEq(feeCurrency.balanceOf(thirdAddress), originalAmount / 2); + + assertEq( + feeCurrencyAdapterForFuzzyTests.balanceOf(address(this)), + (initialSupply - originalAmount) * multiplier + ((originalAmount * multiplier) / 4) + ); + assertEq( + feeCurrencyAdapterForFuzzyTests.balanceOf(secondAddress), + (originalAmount * multiplier) / 4 + ); + assertEq( + feeCurrencyAdapterForFuzzyTests.balanceOf(thirdAddress), + (originalAmount * multiplier) / 2 + ); + } +} + +contract FeeCurrencyAdapter_UpscaleAndDownScaleTests is FeeCurrencyAdapterTest { + function test_shouldUpscale() public { + assertEq(feeCurrencyAdapter.upscaleVisible(1), 1e12); + assertEq(feeCurrencyAdapter.upscaleVisible(1e6), 1e18); + assertEq(feeCurrencyAdapter.upscaleVisible(1e12), 1e24); + } + + function test_ShouldRevertUpscale_WhenOverflow() public { + uint256 digitDifference = 10**12; + uint256 maxValue = type(uint256).max; + uint256 boundaryValue = maxValue / digitDifference + 1; + + vm.expectRevert(); + feeCurrencyAdapter.upscaleVisible(boundaryValue); + } + + function test_shouldDownscale() public { + assertEq(feeCurrencyAdapter.downscaleVisible(1e12), 1); + assertEq(feeCurrencyAdapter.downscaleVisible(1e18), 1e6); + assertEq(feeCurrencyAdapter.downscaleVisible(1e24), 1e12); + } + + function test_ShouldReturn1_WhenSmallEnoughAndRoundingUp() public { + assertEq(feeCurrencyAdapter.downscaleVisible(1), 1); + assertEq(feeCurrencyAdapter.downscaleVisible(1e6 - 1), 1); + assertEq(feeCurrencyAdapter.downscaleVisible(1e12 - 1), 1); + } +} + +contract FeeCurrencyAdapter_SetAdaptedToken is FeeCurrencyAdapterTest { + function test_shouldRevert_WhenNotCalledByOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + feeCurrencyAdapter.setAdaptedToken(address(0)); + } + + function test_shouldSetAdaptedToken() public { + address newWrappedToken = actor("newWrappedToken"); + feeCurrencyAdapter.setAdaptedToken(newWrappedToken); + assertEq(address(feeCurrencyAdapter.adaptedToken()), newWrappedToken); + assertEq(feeCurrencyAdapter.getAdaptedToken(), newWrappedToken); + } +} diff --git a/packages/protocol/test-sol/stability/SortedOracles.mento.t.sol b/packages/protocol/test-sol/stability/SortedOracles.mento.t.sol new file mode 100644 index 00000000000..1b0d77b6871 --- /dev/null +++ b/packages/protocol/test-sol/stability/SortedOracles.mento.t.sol @@ -0,0 +1,659 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.5.13; + +import { Test, console2 as console } from "celo-foundry/Test.sol"; + +import { + SortedLinkedListWithMedian +} from "contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; +import { FixidityLib } from "contracts/common/FixidityLib.sol"; + +import { IBreakerBox } from "../../contracts/stability/interfaces/IBreakerBox.sol"; +import { SortedOracles } from "../../contracts/stability/SortedOracles.sol"; + +contract MockBreakerBox is IBreakerBox { + uint256 public tradingMode; + + function setTradingMode(uint256 _tradingMode) external { + tradingMode = _tradingMode; + } + + function getBreakers() external view returns (address[] memory) { + return new address[](0); + } + + function isBreaker(address) external view returns (bool) { + return true; + } + + function getRateFeedTradingMode(address) external view returns (uint8) { + return 0; + } + + function checkAndSetBreakers(address) external {} +} + +contract SortedOraclesTest is Test { + // Declare SortedOracles events for matching + event ReportExpirySet(uint256 reportExpiry); + event TokenReportExpirySet(address token, uint256 reportExpiry); + event OracleAdded(address indexed token, address indexed oracleAddress); + event OracleRemoved(address indexed token, address indexed oracleAddress); + event OracleReportRemoved(address indexed token, address indexed oracle); + event MedianUpdated(address indexed token, uint256 value); + event OracleReported( + address indexed token, + address indexed oracle, + uint256 timestamp, + uint256 value + ); + event BreakerBoxUpdated(address indexed newBreakerBox); + + SortedOracles sortedOracles; + address owner; + address notOwner; + address rando; + address token; + uint256 aReportExpiry = 3600; + uint256 fixed1 = FixidityLib.unwrap(FixidityLib.fixed1()); + + address oracle; + + bytes32 constant MOCK_EXCHANGE_ID = keccak256(abi.encodePacked("mockExchange")); + + MockBreakerBox mockBreakerBox; + + function setUp() public { + sortedOracles = new SortedOracles(true); + sortedOracles.initialize(aReportExpiry); + + owner = address(this); + notOwner = address(10); + rando = address(2); + token = address(3); + oracle = address(4); + + mockBreakerBox = new MockBreakerBox(); + sortedOracles.setBreakerBox(IBreakerBox(mockBreakerBox)); + vm.startPrank(owner); + currentPrank = owner; + } + + /** + * @notice Test helper function. Submits n Reports for a token from n different Oracles. + */ + + function submitNReports(uint256 n) public { + sortedOracles.addOracle(token, oracle); + changePrank(oracle); + sortedOracles.report(token, fixed1 * 10, address(0), address(0)); + for (uint256 i = 5; i < 5 + n - 1; i++) { + address anotherOracle = address(i); + changePrank(owner); + sortedOracles.addOracle(token, anotherOracle); + changePrank(address(i)); + sortedOracles.report(token, fixed1 * 10, oracle, address(0)); + } + changePrank(owner); + } +} + +/** + * @notice Tests + */ +contract SortedOracles_initialize is SortedOraclesTest { + function test_initialize_shouldHaveSetTheOwner() public { + assertEq(sortedOracles.owner(), owner); + } + + function test_initialize_shouldHaveSetReportExpiryToAReportExpiry() public { + assertEq(sortedOracles.reportExpirySeconds(), aReportExpiry); + } + + function test_initialize_whenCalledAgain_shouldRevert() public { + vm.expectRevert("contract already initialized"); + sortedOracles.initialize(aReportExpiry); + } +} + +contract SortedOracles_setReportExpiry is SortedOraclesTest { + function test_setReportExpiry_shouldUpdateReportExpiry() public { + sortedOracles.setReportExpiry(aReportExpiry + 1); + assertEq(sortedOracles.reportExpirySeconds(), aReportExpiry + 1); + } + + function test_setReportExpiry_shouldEmitEvent() public { + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit ReportExpirySet(aReportExpiry + 1); + sortedOracles.setReportExpiry(aReportExpiry + 1); + } + + function test_setReportExpiry_whenCalledByNonOwner_shouldRevert() public { + vm.expectRevert("Ownable: caller is not the owner"); + changePrank(rando); + sortedOracles.setReportExpiry(aReportExpiry + 1); + } +} + +contract SortedOracles_setTokenReportExpiry is SortedOraclesTest { + uint256 aNewReportExpiry = aReportExpiry + 1; + + function test_setTokenReportExpiry_shouldUpdateTokenReportExpiry() public { + sortedOracles.setTokenReportExpiry(token, aNewReportExpiry); + assertEq(sortedOracles.tokenReportExpirySeconds(token), aNewReportExpiry); + } + + function test_setTokenReportExpiry_shouldEmitTokenReportExpirySetEvent() public { + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit TokenReportExpirySet(token, aNewReportExpiry); + sortedOracles.setTokenReportExpiry(token, aNewReportExpiry); + } + + function test_setTokenReportExpiry_whenCalledByNonOwner_shouldRevert() public { + vm.expectRevert("Ownable: caller is not the owner"); + changePrank(rando); + sortedOracles.setTokenReportExpiry(token, aNewReportExpiry); + } +} + +contract SortedOracles_getTokenReportExpiry is SortedOraclesTest { + function test_getTokenReportExpirySeconds_whenNoTokenLevelExpiryIsSet_shouldReturnContractLevel() + public + { + assertEq(sortedOracles.getTokenReportExpirySeconds(token), aReportExpiry); + } + + function test_getTokenReportExpirySeconds_whenTokenLevelExpiryIsSet_shouldReturnTokenLevel() + public + { + sortedOracles.setTokenReportExpiry(token, aReportExpiry + 1); + assertEq(sortedOracles.getTokenReportExpirySeconds(token), aReportExpiry + 1); + } +} + +contract SortedOracles_addOracles is SortedOraclesTest { + function test_addOracle_shouldAddAnOracle() public { + sortedOracles.addOracle(token, oracle); + assertTrue(sortedOracles.isOracle(token, oracle)); + } + + function test_addOracle_shouldEmitEvent() public { + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit OracleAdded(token, oracle); + sortedOracles.addOracle(token, oracle); + } + + function test_addOracle_whenTokenIsTheNullAddress_shouldRevert() public { + vm.expectRevert( + "token addr was null or oracle addr was null or oracle addr is already an oracle for token addr" + ); + sortedOracles.addOracle(address(0), oracle); + } + + function test_addOracle_whenOracleIsTheNullAddress_shouldRevert() public { + vm.expectRevert( + "token addr was null or oracle addr was null or oracle addr is already an oracle for token addr" + ); + sortedOracles.addOracle(token, address(0)); + } + + function test_addOracle_whenOracleHasBeenAdded_shouldRevert() public { + sortedOracles.addOracle(token, oracle); + vm.expectRevert( + "token addr was null or oracle addr was null or oracle addr is already an oracle for token addr" + ); + sortedOracles.addOracle(token, oracle); + } + + function test_addOracle_whenCalledByNonOwner_shouldRevert() public { + vm.expectRevert("Ownable: caller is not the owner"); + changePrank(rando); + sortedOracles.addOracle(token, oracle); + } +} + +contract SortedOracles_breakerBox is SortedOraclesTest { + function test_setBreakerBox_whenCalledByNonOwner_shouldRevert() public { + changePrank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + sortedOracles.setBreakerBox(MockBreakerBox(address(0))); + } + + function test_setBreakerBox_whenGivenAddressIsNull_shouldRevert() public { + vm.expectRevert("BreakerBox address must be set"); + sortedOracles.setBreakerBox(MockBreakerBox(address(0))); + } + + function test_setBreakerBox_shouldUpdateAndEmit() public { + sortedOracles = new SortedOracles(true); + assertEq(address(sortedOracles.breakerBox()), address(0)); + vm.expectEmit(true, true, true, true); + emit BreakerBoxUpdated(address(mockBreakerBox)); + + sortedOracles.setBreakerBox(mockBreakerBox); + assertEq(address(sortedOracles.breakerBox()), address(mockBreakerBox)); + } +} + +contract SortedOracles_RemoveOracles is SortedOraclesTest { + function test_removeOracle_shouldRemoveAnOracle() public { + sortedOracles.addOracle(token, oracle); + sortedOracles.removeOracle(token, oracle, 0); + assertFalse(sortedOracles.isOracle(token, oracle)); + } + + function test_removeOracle_whenMoreThanOneReportExists_shouldDecreaseNumberOfRates() public { + submitNReports(2); + sortedOracles.removeOracle(token, oracle, 0); + assertEq(sortedOracles.numRates(token), 1); + } + + function test_removeOracle_whenMoreThanOneReportExists_shouldDecreaseNumberOfTimestamps() public { + submitNReports(2); + sortedOracles.removeOracle(token, oracle, 0); + assertEq(sortedOracles.numTimestamps(token), 1); + } + + function test_removeOracle_whenMoreThanOneReportExists_shouldEmitOracleRemovedOracleReportRemovedMedianUpdatedEvent() + public + { + submitNReports(1); + sortedOracles.addOracle(token, address(6)); + changePrank(address(6)); + sortedOracles.report(token, fixed1 * 12, oracle, address(0)); + + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit OracleReportRemoved(token, address(6)); + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit MedianUpdated(token, fixed1 * 10); + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit OracleRemoved(token, address(6)); + + changePrank(owner); + sortedOracles.removeOracle(token, address(6), 1); + } + + function test_removeOracle_whenOneReportExists_shouldNotDecreaseNumberOfRates() public { + submitNReports(1); + sortedOracles.removeOracle(token, oracle, 0); + assertEq(sortedOracles.numRates(token), 1); + } + + function test_removeOracle_whenOneReportExists_shouldNotResetTheMedianRate() public { + submitNReports(1); + (uint256 numeratorBefore, ) = sortedOracles.medianRate(token); + sortedOracles.removeOracle(token, oracle, 0); + (uint256 numeratorAfter, ) = sortedOracles.medianRate(token); + assertEq(numeratorBefore, numeratorAfter); + } + + function test_removeOracle_whenOneReportExists_shouldNotDecreaseNumberOfTimestamps() public { + submitNReports(1); + sortedOracles.removeOracle(token, oracle, 0); + assertEq(sortedOracles.numTimestamps(token), 1); + } + + function test_removeOracle_whenOneReportExists_shouldNotResetTheMedianTimestamp() public { + submitNReports(1); + uint256 medianTimestampBefore = sortedOracles.medianTimestamp(token); + sortedOracles.removeOracle(token, oracle, 0); + uint256 medianTimestampAfter = sortedOracles.medianTimestamp(token); + assertEq(medianTimestampBefore, medianTimestampAfter); + } + + function testFail_removeOracle_whenOneReportExists_shouldNotEmitTheOracleReportedAndMedianUpdatedEvent() + public + { + // testFail feals impricise here. + // TODO: Better way of testing this case :) + submitNReports(1); + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit OracleReportRemoved(token, oracle); + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit MedianUpdated(token, 0); + sortedOracles.removeOracle(token, oracle, 0); + } + + function test_removeOracle_whenOneReportExists_shouldEmitTheOracleRemovedEvent() public { + submitNReports(1); + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit OracleRemoved(token, oracle); + sortedOracles.removeOracle(token, oracle, 0); + } + + function test_removeOracle_whenIndexIsWrong_shouldRevert() public { + submitNReports(1); + vm.expectRevert( + "token addr null or oracle addr null or index of token oracle not mapped to oracle addr" + ); + sortedOracles.removeOracle(token, oracle, 1); + } + + function test_removeOracle_whenAddressIsWrong_shouldRevert() public { + submitNReports(1); + vm.expectRevert( + "token addr null or oracle addr null or index of token oracle not mapped to oracle addr" + ); + sortedOracles.removeOracle(token, address(15), 0); + } + + function test_removeOracle_whenCalledByNonOwner_shouldRevert() public { + submitNReports(1); + vm.expectRevert("Ownable: caller is not the owner"); + changePrank(rando); + sortedOracles.removeOracle(token, address(15), 0); + } +} + +contract SortedOracles_removeExpiredReports is SortedOraclesTest { + function test_removeExpiredReports_whenNoReportExists_shouldRevert() public { + sortedOracles.addOracle(token, oracle); + vm.expectRevert("token addr null or trying to remove too many reports"); + sortedOracles.removeExpiredReports(token, 1); + } + + function test_removeExpiredReports_whenOnlyOneReportExists_shouldRevert() public { + sortedOracles.addOracle(token, oracle); + changePrank(oracle); + sortedOracles.report(token, fixed1, address(0), address(0)); + vm.expectRevert("token addr null or trying to remove too many reports"); + sortedOracles.removeExpiredReports(token, 1); + } + + function test_removeExpiredReports_whenOldestReportIsNotExpired_shouldDoNothing() public { + submitNReports(5); + sortedOracles.removeExpiredReports(token, 3); + assertEq(sortedOracles.numTimestamps(token), 5); + } + + function test_removeExpiredReports_whenLessThanNReportsAreExpired_shouldRemoveAllExpiredAndStop() + public + { + //first 5 expired reports + submitNReports(5); + skip(aReportExpiry); + //two reports that aren't expired + sortedOracles.addOracle(token, address(10)); + changePrank(address(10)); + sortedOracles.report(token, fixed1 * 10, oracle, address(0)); + changePrank(owner); + sortedOracles.addOracle(token, address(11)); + changePrank(address(11)); + sortedOracles.report(token, fixed1 * 10, oracle, address(0)); + + changePrank(owner); + sortedOracles.removeExpiredReports(token, 6); + assertEq(sortedOracles.numTimestamps(token), 2); + } + + function test_removeExpiredReports_whenNLargerThanNumberOfTimestamps_shouldRevert() public { + submitNReports(5); + vm.expectRevert("token addr null or trying to remove too many reports"); + sortedOracles.removeExpiredReports(token, 7); + } + + function test_removeExpiredReports_whenNReportsAreExpired_shouldRemoveNReports() public { + submitNReports(6); + skip(aReportExpiry); + sortedOracles.removeExpiredReports(token, 5); + assertEq(sortedOracles.numTimestamps(token), 1); + } + + function test_removeExpiredReports_whenMoreThanOneReportExistsAndMedianUpdated_shouldCallCheckAndSetBreakers() + public + { + submitNReports(2); + sortedOracles.addOracle(token, address(6)); + changePrank(address(6)); + + vm.warp(now + aReportExpiry); + sortedOracles.report(token, fixed1 * 12, oracle, address(0)); + + vm.expectEmit(false, false, false, false, address(sortedOracles)); + emit OracleReportRemoved(token, rando); + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit MedianUpdated(token, fixed1 * 12); + vm.expectCall( + address(mockBreakerBox), + abi.encodeWithSelector(mockBreakerBox.checkAndSetBreakers.selector) + ); + + sortedOracles.removeExpiredReports(token, 2); + } +} + +contract SortedOracles_isOldestReportExpired is SortedOraclesTest { + function test_isOldestReportExpired_whenNoReportsExist_shouldReturnTrue() public { + //added this skip because foundry starts at a block time of 0 + //without the skip isOldestReortExpired would return false when no reports exist + skip(aReportExpiry); + sortedOracles.addOracle(token, oracle); + (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertTrue(isReportExpired); + } + + function test_isOldestReportExpired_whenReportIsExpired_shouldReturnTrue() public { + submitNReports(1); + skip(aReportExpiry); + (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertTrue(isReportExpired); + } + + function test_isOldestReportExpired_whenReportIsntExpired_shouldReturnFalse() public { + submitNReports(1); + (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertFalse(isReportExpired); + } + + function test_isOldestReportExpired_whenTokenSpecificExpiryIsntExceeded_shoulReturnFalse() + public + { + submitNReports(1); + sortedOracles.setTokenReportExpiry(token, aReportExpiry * 2); + //neither general nor specific Expiry expired + (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertFalse(isReportExpired); + //general Expiry expired but not specific + skip(aReportExpiry); + (isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertFalse(isReportExpired); + } + + function test_isOldestReportExpired_whenSpecificTokenExpiryIsExceeded_shouldReturnTrue() public { + submitNReports(1); + sortedOracles.setTokenReportExpiry(token, aReportExpiry * 2); + skip(aReportExpiry * 2); + (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertTrue(isReportExpired); + } + + function test_isOldestReportExpired_whenSpecificExpiryIsLowerButNotExpired_shouldReturnFalse() + public + { + submitNReports(1); + sortedOracles.setTokenReportExpiry(token, (aReportExpiry * 1) / 2); + (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertFalse(isReportExpired); + } + + function test_isOldestReportExpired_whenSpecificExpiryIsLowerAndGeneralExpiryIsExceeded_shouldReturnTrue() + public + { + submitNReports(1); + sortedOracles.setTokenReportExpiry(token, (aReportExpiry * 1) / 2); + skip(aReportExpiry); + (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertTrue(isReportExpired); + } + + function test_isOldestReportExpired_whenSpecificExpiryIsLowerAndSpecificExpiryIsExceeded_shouldReturnTrue() + public + { + submitNReports(1); + sortedOracles.setTokenReportExpiry(token, (aReportExpiry * 1) / 2); + skip((aReportExpiry * 1) / 2); + (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); + assertTrue(isReportExpired); + } +} + +contract SortedOracles_report is SortedOraclesTest { + address oracleB = actor("oracleB"); + address oracleC = actor("oracleC"); + + function test_report_shouldIncreaseTheNumberOfRates() public { + assertEq(sortedOracles.numRates(token), 0); + submitNReports(1); + assertEq(sortedOracles.numRates(token), 1); + } + + function test_report_shouldSetTheMedianRate() public { + sortedOracles.addOracle(token, oracle); + changePrank(oracle); + sortedOracles.report(token, fixed1 * 10, address(0), address(0)); + (uint256 numerator, uint256 denominator) = sortedOracles.medianRate(token); + assertEq(numerator, fixed1 * 10); + assertEq(denominator, fixed1); + } + + function test_report_shouldIncreaseTheNumberOfTimestamps() public { + assertEq(sortedOracles.numTimestamps(token), 0); + submitNReports(1); + assertEq(sortedOracles.numTimestamps(token), 1); + } + + function test_report_shouldSetTheMedianTimestamp() public { + submitNReports(1); + assertEq(block.timestamp, sortedOracles.medianTimestamp(token)); + } + + function test_report_shouldEmitTheOracleReportedAndMedianUpdatedEvent() public { + sortedOracles.addOracle(token, oracle); + vm.expectEmit(true, true, true, true, address(sortedOracles)); + emit OracleReported(token, oracle, block.timestamp, fixed1 * 10); + emit MedianUpdated(token, fixed1 * 10); + changePrank(oracle); + sortedOracles.report(token, fixed1 * 10, address(0), address(0)); + } + + function test_report_whenCalledByNonOracle_shouldRevert() public { + changePrank(rando); + vm.expectRevert("sender was not an oracle for token addr"); + sortedOracles.report(token, fixed1, address(0), address(0)); + } + + function test_report_whenOneReportBySameOracleExists_shouldResetMedianRate() public { + submitNReports(1); + (uint256 numerator, uint256 denominator) = sortedOracles.medianRate(token); + assertEq(numerator, fixed1 * 10); + assertEq(denominator, fixed1); + + changePrank(oracle); + sortedOracles.report(token, fixed1 * 20, address(0), address(0)); + (numerator, denominator) = sortedOracles.medianRate(token); + assertEq(numerator, fixed1 * 20); + assertEq(denominator, fixed1); + } + + function test_report_whenOneReportBySameOracleExists_shouldNotChangeNumberOfTotalReports() + public + { + submitNReports(1); + uint256 initialNumberOfReports = sortedOracles.numRates(token); + changePrank(oracle); + sortedOracles.report(token, fixed1 * 20, address(0), address(0)); + assertEq(initialNumberOfReports, sortedOracles.numRates(token)); + } + + function test_report_whenMultipleReportsExistTheMostRecent_shouldUpdateListOfRatesCorrectly() + public + { + address anotherOracle = address(5); + uint256 oracleValue1 = fixed1; + uint256 oracleValue2 = fixed1 * 2; + uint256 oracleValue3 = fixed1 * 3; + sortedOracles.addOracle(token, anotherOracle); + sortedOracles.addOracle(token, oracle); + + changePrank(anotherOracle); + sortedOracles.report(token, oracleValue1, address(0), address(0)); + changePrank(oracle); + sortedOracles.report(token, oracleValue2, anotherOracle, address(0)); + + //confirm correct setUp + changePrank(owner); + (address[] memory oracles, uint256[] memory rates, ) = sortedOracles.getRates(token); + assertEq(oracle, oracles[0]); + assertEq(oracleValue2, rates[0]); + assertEq(anotherOracle, oracles[1]); + assertEq(oracleValue1, rates[1]); + + changePrank(oracle); + sortedOracles.report(token, oracleValue3, anotherOracle, address(0)); + + changePrank(owner); + (oracles, rates, ) = sortedOracles.getRates(token); + assertEq(oracle, oracles[0]); + assertEq(oracleValue3, rates[0]); + assertEq(anotherOracle, oracles[1]); + assertEq(oracleValue1, rates[1]); + } + + function test_report_whenMultipleReportsExistTheMostRecent_shouldUpdateTimestampsCorrectly() + public + { + address anotherOracle = address(5); + uint256 oracleValue1 = fixed1; + uint256 oracleValue2 = fixed1 * 2; + uint256 oracleValue3 = fixed1 * 3; + sortedOracles.addOracle(token, anotherOracle); + sortedOracles.addOracle(token, oracle); + + changePrank(anotherOracle); + uint256 timestamp0 = block.timestamp; + sortedOracles.report(token, oracleValue1, address(0), address(0)); + skip(5); + + uint256 timestamp1 = block.timestamp; + changePrank(oracle); + sortedOracles.report(token, oracleValue2, anotherOracle, address(0)); + skip(5); + + //confirm correct setUp + changePrank(owner); + (address[] memory oracles, uint256[] memory timestamps, ) = sortedOracles.getTimestamps(token); + assertEq(oracle, oracles[0]); + assertEq(timestamp1, timestamps[0]); + assertEq(anotherOracle, oracles[1]); + assertEq(timestamp0, timestamps[1]); + + changePrank(oracle); + uint256 timestamp3 = block.timestamp; + sortedOracles.report(token, oracleValue3, anotherOracle, address(0)); + + changePrank(owner); + (oracles, timestamps, ) = sortedOracles.getTimestamps(token); + assertEq(oracle, oracles[0]); + assertEq(timestamp3, timestamps[0]); + + assertEq(anotherOracle, oracles[1]); + assertEq(timestamp0, timestamps[1]); + } + + function test_report_shouldCallBreakerBoxWithRateFeedID() public { + // token is a legacy reference of rateFeedID + sortedOracles.addOracle(token, oracle); + sortedOracles.setBreakerBox(mockBreakerBox); + + vm.expectCall( + address(mockBreakerBox), + abi.encodeWithSelector(mockBreakerBox.checkAndSetBreakers.selector, token) + ); + + changePrank(oracle); + + sortedOracles.report(token, 9999, address(0), address(0)); + } +} diff --git a/packages/protocol/test-sol/stability/SortedOracles.t.sol b/packages/protocol/test-sol/stability/SortedOracles.t.sol index 00895163854..3a650adba63 100644 --- a/packages/protocol/test-sol/stability/SortedOracles.t.sol +++ b/packages/protocol/test-sol/stability/SortedOracles.t.sol @@ -8,6 +8,7 @@ import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol"; import "@celo-contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; import { Constants } from "../constants.sol"; +import "forge-std/console.sol"; contract SortedOraclesTest is Test, Constants { using FixidityLib for FixidityLib.Fraction; @@ -17,7 +18,7 @@ contract SortedOraclesTest is Test, Constants { address oracleAccount; address aToken = 0x00000000000000000000000000000000DeaDBeef; - uint256 reportExpiry = 1 * 60 * 60; // 1 hour + uint256 reportExpiry = HOUR; event OracleAdded(address indexed token, address indexed oracleAddress); event OracleRemoved(address indexed token, address indexed oracleAddress); @@ -31,6 +32,7 @@ contract SortedOraclesTest is Test, Constants { event MedianUpdated(address indexed token, uint256 value); event ReportExpirySet(uint256 reportExpiry); event TokenReportExpirySet(address token, uint256 reportExpiry); + event EquivalentTokenSet(address indexed token, address indexed equivalentToken); function setUp() public { warp(0); @@ -44,7 +46,7 @@ contract SortedOraclesTest is Test, Constants { } } -contract Initialize is SortedOraclesTest { +contract SortedOraclesTest_Initialize is SortedOraclesTest { function test_ownerSet() public { assertEq(sortedOracle.owner(), address(this)); } @@ -53,7 +55,7 @@ contract Initialize is SortedOraclesTest { assertEq(sortedOracle.reportExpirySeconds(), reportExpiry); } - function test_ShouldRevertWhenCalledAgain() public { + function test_ShouldRevert_WhenCalledAgain() public { vm.expectRevert("contract already initialized"); sortedOracle.initialize(reportExpiry); } @@ -80,6 +82,67 @@ contract SetReportExpiry is SortedOraclesTest { } } +contract SortedOracles_SetEquivalentToken is SortedOraclesTest { + address bToken = actor("bToken"); + + function test_ShouldSetReportExpiry() public { + sortedOracle.setEquivalentToken(aToken, bToken); + address equivalentToken = sortedOracle.getEquivalentToken(aToken); + assertEq(equivalentToken, bToken); + } + + function test_ShouldRevert_WhenToken0() public { + vm.expectRevert("token address cannot be 0"); + sortedOracle.setEquivalentToken(address(0), bToken); + } + + function test_ShouldRevert_WhenEquivalentToken0() public { + vm.expectRevert("equivalentToken address cannot be 0"); + sortedOracle.setEquivalentToken(aToken, address(0)); + } + + function test_ShouldEmitEquivalentTokenSet() public { + vm.expectEmit(true, true, true, true); + emit EquivalentTokenSet(aToken, bToken); + sortedOracle.setEquivalentToken(aToken, bToken); + } + + function test_ShouldRevertWhenNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(oracleAccount); + sortedOracle.setEquivalentToken(aToken, bToken); + } +} + +contract SortedOracles_DeleteEquivalentToken is SortedOraclesTest { + address bToken = actor("bToken"); + + function test_ShouldDeleteEquivalentToken() public { + sortedOracle.setEquivalentToken(aToken, bToken); + sortedOracle.deleteEquivalentToken(aToken); + address equivalentToken = sortedOracle.getEquivalentToken(aToken); + assertEq(equivalentToken, address(0)); + } + + function test_ShouldRevert_WhenEquivalentToken0() public { + vm.expectRevert("token address cannot be 0"); + sortedOracle.deleteEquivalentToken(address(0)); + } + + function test_ShouldEmitEquivalentTokenSet() public { + sortedOracle.setEquivalentToken(aToken, bToken); + vm.expectEmit(true, true, true, true); + emit EquivalentTokenSet(aToken, address(0)); + sortedOracle.deleteEquivalentToken(aToken); + } + + function test_ShouldRevertWhenNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(oracleAccount); + sortedOracle.deleteEquivalentToken(aToken); + } +} + contract SetTokenReportExpiry is SortedOraclesTest { function test_ShouldSetTokenReportExpiry() public { uint256 newReportExpiry = reportExpiry * 2; @@ -121,17 +184,23 @@ contract AddOracle is SortedOraclesTest { function test_ShouldRevertWhenAlreadyOracle() public { sortedOracle.addOracle(aToken, oracleAccount); - vm.expectRevert("oracle addr is not an oracle for token addr"); + vm.expectRevert( + "token addr was null or oracle addr was null or oracle addr is already an oracle for token addr" + ); sortedOracle.addOracle(aToken, oracleAccount); } function test_ShouldRevertWhenOracleIsZeroAddress() public { - vm.expectRevert("oracle addr was null"); + vm.expectRevert( + "token addr was null or oracle addr was null or oracle addr is already an oracle for token addr" + ); sortedOracle.addOracle(aToken, address(0)); } function test_ShouldRevertWhenTokenIsZeroAddress() public { - vm.expectRevert("token addr was null"); + vm.expectRevert( + "token addr was null or oracle addr was null or oracle addr is already an oracle for token addr" + ); sortedOracle.addOracle(address(0), oracleAccount); } } @@ -331,6 +400,9 @@ contract RemoveOracle is SortedOraclesTest { function test_ShouldRemoveOracle() public { sortedOracle.removeOracle(aToken, oracleAccount, 0); assertEq(sortedOracle.isOracle(aToken, oracleAccount), false); + + address[] memory oracles = sortedOracle.getOracles(aToken); + assertEq(oracles.length, 0); } function helper_WhenThereIsMoreThanOneReportMade() public { @@ -473,6 +545,8 @@ contract Report is SortedOraclesTest { uint256 oracleValue2 = FixidityLib.newFixedFraction(3, 1).unwrap(); uint256 anotherOracleValue = FIXED1; + address bToken = actor("bToken"); + function setUp() public { super.setUp(); sortedOracle.addOracle(aToken, oracleAccount); @@ -492,6 +566,43 @@ contract Report is SortedOraclesTest { assertEq(denominator, FIXED1); } + function test_ShouldReturnTheMedianRate_WhenEquivalentTokenIsSet() public { + vm.prank(oracleAccount); + sortedOracle.report(aToken, value, address(0), address(0)); + sortedOracle.setEquivalentToken(bToken, aToken); + (uint256 medianRate, uint256 denominator) = sortedOracle.medianRate(bToken); + assertEq(medianRate, value); + assertEq(denominator, FIXED1); + } + + function test_ShouldNotReturnTheMedianRate_WhenEquivalentTokenIsSet() public { + vm.prank(oracleAccount); + sortedOracle.report(aToken, value, address(0), address(0)); + sortedOracle.setEquivalentToken(bToken, aToken); + (uint256 medianRate, uint256 denominator) = sortedOracle.medianRateWithoutEquivalentMapping( + bToken + ); + assertEq(medianRate, 0); + assertEq(denominator, 0); + } + + function test_ShouldNotReturnTheMedianRateOfEquivalentToken_WhenEquivalentTokenIsSetAndDeleted() + public + { + vm.prank(oracleAccount); + sortedOracle.report(aToken, value, address(0), address(0)); + sortedOracle.setEquivalentToken(bToken, aToken); + uint256 medianRate; + uint256 denominator; + (medianRate, denominator) = sortedOracle.medianRate(bToken); + assertEq(medianRate, value); + assertEq(denominator, FIXED1); + sortedOracle.deleteEquivalentToken(bToken); + (medianRate, denominator) = sortedOracle.medianRate(bToken); + assertEq(medianRate, 0); + assertEq(denominator, 0); + } + function test_ShouldIncreaseTheNumberOfTimestamps() public { vm.prank(oracleAccount); sortedOracle.report(aToken, value, address(0), address(0)); diff --git a/packages/protocol/test/common/integration.ts b/packages/protocol/test/common/integration.ts index 474d93e0d72..0e8b775a3f6 100644 --- a/packages/protocol/test/common/integration.ts +++ b/packages/protocol/test/common/integration.ts @@ -668,18 +668,12 @@ contract('Integration: Adding StableToken', (accounts: string[]) => { it(`should be impossible to sell CELO`, async () => { await goldToken.approve(exchangeAbc.address, sellAmount) - await assertTransactionRevertWithReason( - exchangeAbc.sell(sellAmount, minBuyAmount, true), - 'token address cannot be null' - ) + await assertTransactionRevertWithReason(exchangeAbc.sell(sellAmount, minBuyAmount, true)) }) it(`should be impossible to sell stable token`, async () => { await stableTokenAbc.approve(exchangeAbc.address, sellAmount) - await assertTransactionRevertWithReason( - exchangeAbc.sell(sellAmount, minBuyAmount, false), - 'token address cannot be null' - ) + await assertTransactionRevertWithReason(exchangeAbc.sell(sellAmount, minBuyAmount, false)) }) }) diff --git a/packages/protocol/test/compatibility/ast-code.ts b/packages/protocol/test/compatibility/ast-code.ts index 12d61960267..300177786d1 100644 --- a/packages/protocol/test/compatibility/ast-code.ts +++ b/packages/protocol/test/compatibility/ast-code.ts @@ -35,21 +35,21 @@ const comp = (c1: Change, c2: Change): number => { describe('#reportASTIncompatibilities()', () => { describe('when the contracts are the same', () => { it('reports no changes', () => { - const report = reportASTIncompatibilities(testCases.original, [testCases.original_copy]) + const report = reportASTIncompatibilities([testCases.original], [testCases.original_copy]) assert.isEmpty(report.getChanges()) }) }) describe('when only metadata has changed', () => { it('reports no changes', () => { - const report = reportASTIncompatibilities(testCases.original, [testCases.metadata_changed]) + const report = reportASTIncompatibilities([testCases.original], [testCases.metadata_changed]) assert.isEmpty(report.getChanges()) }) }) describe('when a contract storage is changed', () => { it('reports only bytecode changes', () => { - const report = reportASTIncompatibilities(testCases.original, [testCases.inserted_constant]) + const report = reportASTIncompatibilities([testCases.original], [testCases.inserted_constant]) const expected = [new DeployedBytecodeChange('TestContract')] assert.deepEqual(report.getChanges(), expected) }) @@ -57,9 +57,10 @@ describe('#reportASTIncompatibilities()', () => { describe('when a contract and methods are added', () => { it('reports proper changes', () => { - const report = reportASTIncompatibilities(testCases.original, [ - testCases.added_methods_and_contracts, - ]) + const report = reportASTIncompatibilities( + [testCases.original], + [testCases.added_methods_and_contracts] + ) const expected = [ new NewContractChange('TestContractNew'), new DeployedBytecodeChange('TestContract'), @@ -74,9 +75,10 @@ describe('#reportASTIncompatibilities()', () => { describe('when methods are removed', () => { it('reports proper changes', () => { - const report = reportASTIncompatibilities(testCases.added_methods_and_contracts, [ - testCases.original, - ]) + const report = reportASTIncompatibilities( + [testCases.added_methods_and_contracts], + [testCases.original] + ) const expected = [ new DeployedBytecodeChange('TestContract'), new MethodRemovedChange('TestContract', 'newMethod1(uint256)'), @@ -90,9 +92,10 @@ describe('#reportASTIncompatibilities()', () => { describe('when many changes are made', () => { it('reports proper changes', () => { - const report = reportASTIncompatibilities(testCases.big_original, [ - testCases.big_original_modified, - ]) + const report = reportASTIncompatibilities( + [testCases.big_original], + [testCases.big_original_modified] + ) const expected = [ new NewContractChange('NewContract'), new DeployedBytecodeChange('ImplementationChangeContract'), diff --git a/packages/protocol/test/compatibility/ast-layout.ts b/packages/protocol/test/compatibility/ast-layout.ts index 4c4520b2bca..9f4f0c0a126 100644 --- a/packages/protocol/test/compatibility/ast-layout.ts +++ b/packages/protocol/test/compatibility/ast-layout.ts @@ -68,30 +68,31 @@ const assertContractErrorsMatch = (report, contractName: string, expectedMatches describe('#reportLayoutIncompatibilities()', () => { describe('when the contracts are the same', () => { it('reports no incompatibilities', () => { - const report = reportLayoutIncompatibilities(testCases.original, [testCases.original]) + const report = reportLayoutIncompatibilities([testCases.original], [testCases.original]) assertCompatible(report) }) }) describe('when a constant is inserted in a contract', () => { it('reports no incompatibilities', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.inserted_constant, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.inserted_constant] + ) assertCompatible(report) }) }) describe('when a variable is appended in a contract', () => { it('reports no incompatibilities', () => { - const report = reportLayoutIncompatibilities(testCases.original, [testCases.appended]) + const report = reportLayoutIncompatibilities([testCases.original], [testCases.appended]) assertCompatible(report) }) }) describe('when a variable is inserted in a contract', () => { it('reports an inserted variable', () => { - const report = reportLayoutIncompatibilities(testCases.original, [testCases.inserted]) + const report = reportLayoutIncompatibilities([testCases.original], [testCases.inserted]) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/inserted/]) }) @@ -99,9 +100,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a variable is appended in a parent contract', () => { it('reports an inserted variable', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.appended_in_parent, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.appended_in_parent] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/inserted/]) }) @@ -109,7 +111,7 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a variable is removed in a contract', () => { it('reports a removed variable', () => { - const report = reportLayoutIncompatibilities(testCases.original, [testCases.removed]) + const report = reportLayoutIncompatibilities([testCases.original], [testCases.removed]) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/removed/]) }) @@ -117,9 +119,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a variable is removed in a parent contract', () => { it('reports a removed variable', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.removed_from_parent, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.removed_from_parent] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/removed/]) }) @@ -127,7 +130,7 @@ describe('#reportLayoutIncompatibilities()', () => { describe(`when a variable's type changes in a contract`, () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original, [testCases.typechange]) + const report = reportLayoutIncompatibilities([testCases.original], [testCases.typechange]) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) @@ -135,9 +138,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe(`when a variable's type changes in a parent contract`, () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.typechange_in_parent, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.typechange_in_parent] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) @@ -145,54 +149,60 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a field is added to a struct in mapping', () => { it('reports no incompatibilities', () => { - const report = reportLayoutIncompatibilities(testCases.original_struct_in_mapping, [ - testCases.inserted_in_struct_mapping, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_struct_in_mapping], + [testCases.inserted_in_struct_mapping] + ) assertCompatible(report) }) }) describe('when a field is added to a library struct in mapping', () => { it('reports no incompatibilities', () => { - const report = reportLayoutIncompatibilities(testCases.original_struct_in_mapping, [ - testCases.inserted_in_library_struct_mapping, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_struct_in_mapping], + [testCases.inserted_in_library_struct_mapping] + ) assertCompatible(report) }) }) describe('when a field is prefixed with deprecated to a library struct in mapping', () => { it('reports no incompatibilities', () => { - const report = reportLayoutIncompatibilities(testCases.original_struct_in_mapping, [ - testCases.deprecated_prefixed_in_library_struct_mapping, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_struct_in_mapping], + [testCases.deprecated_prefixed_in_library_struct_mapping] + ) assertCompatible(report) }) }) describe('when a field is prefixed with deprecated to struct variable', () => { it('reports no incompatibilities', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.deprecated_prefixed_in_struct, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.deprecated_prefixed_in_struct] + ) assertCompatible(report) }) }) describe('when a variable is prefixed with deprecated', () => { it('reports no incompatibilities', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.deprecated_prefixed_variable, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.deprecated_prefixed_variable] + ) assertCompatible(report) }) }) describe('when a field is added to a struct', () => { it('reports a struct change', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.inserted_in_struct, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.inserted_in_struct] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/struct.*changed/]) }) @@ -200,9 +210,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a field changes type in a struct', () => { it('reports a struct change', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.typechange_in_struct, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.typechange_in_struct] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/struct.*changed/]) }) @@ -210,9 +221,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a field changes type in a library struct', () => { it('reports a struct change', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.typechange_in_library_struct, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.typechange_in_library_struct] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/struct.*changed/]) }) @@ -220,9 +232,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a field is removed from a struct', () => { it('reports a struct change', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.removed_from_struct, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.removed_from_struct] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/struct.*changed/]) }) @@ -230,9 +243,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a field is removed from a library struct', () => { it('reports a struct change', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.removed_from_library_struct, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.removed_from_library_struct] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/struct.*changed/]) }) @@ -240,9 +254,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a field is inserted in a library struct', () => { it('reports a struct change', () => { - const report = reportLayoutIncompatibilities(testCases.original, [ - testCases.inserted_in_library_struct, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original], + [testCases.inserted_in_library_struct] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/struct.*changed/]) }) @@ -250,9 +265,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a fixed array has length increased', () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original_complex, [ - testCases.longer_fixed_array, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_complex], + [testCases.longer_fixed_array] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) @@ -260,9 +276,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a fixed array has length decreased', () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original_complex, [ - testCases.shorter_fixed_array, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_complex], + [testCases.shorter_fixed_array] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) @@ -270,9 +287,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a fixed array becomes dynamic', () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original_complex, [ - testCases.fixed_to_dynamic_array, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_complex], + [testCases.fixed_to_dynamic_array] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) @@ -280,9 +298,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when a dynamic array becomes fixed', () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original_complex, [ - testCases.dynamic_to_fixed_array, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_complex], + [testCases.dynamic_to_fixed_array] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) @@ -294,9 +313,10 @@ describe('#reportLayoutIncompatibilities()', () => { // incompatibility. describe.skip('when the source of a mapping changes', () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original_complex, [ - testCases.mapping_source_changed, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_complex], + [testCases.mapping_source_changed] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) @@ -304,9 +324,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe.skip('when the source of a nested mapping changes', () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original_complex, [ - testCases.internal_mapping_source_changed, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_complex], + [testCases.internal_mapping_source_changed] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) @@ -314,9 +335,10 @@ describe('#reportLayoutIncompatibilities()', () => { describe('when the target of a mapping changes', () => { it('reports a typechanged variable', () => { - const report = reportLayoutIncompatibilities(testCases.original_complex, [ - testCases.mapping_target_changed, - ]) + const report = reportLayoutIncompatibilities( + [testCases.original_complex], + [testCases.mapping_target_changed] + ) assertNotCompatible(report) assertContractErrorsMatch(report, 'TestContract', [/had type/]) }) diff --git a/packages/protocol/test/compatibility/library-linking.ts b/packages/protocol/test/compatibility/library-linking.ts index 3e5f5b4ec8e..b3c446bac69 100644 --- a/packages/protocol/test/compatibility/library-linking.ts +++ b/packages/protocol/test/compatibility/library-linking.ts @@ -11,9 +11,10 @@ const testCases = { describe('reportLibraryLinkingIncompatibilities', () => { it('detects when a linked library has changed', () => { - const codeReport = reportASTIncompatibilities(testCases.linked_libraries, [ - testCases.linked_libraries_upgraded_lib, - ]) + const codeReport = reportASTIncompatibilities( + [testCases.linked_libraries], + [testCases.linked_libraries_upgraded_lib] + ) const libraryLinksReport = reportLibraryLinkingIncompatibilities( { LinkedLibrary1: ['TestContract'], diff --git a/yarn.lock b/yarn.lock index 67c46cb6016..eef85441a72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18977,7 +18977,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -19003,15 +19003,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -19103,7 +19094,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19131,13 +19122,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -21863,7 +21847,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -21889,15 +21873,6 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"