diff --git a/README.md b/README.md index 4baf937..42397e5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ Please note all these oracles are designed for consumption by `AaveOracle` which [PotRateSource](https://github.com/marsfoundation/sparklend-advanced/blob/master/src/PotRateSource.sol): Adapter to convert DSR into APR which can be consumed by one of the rate target interest rate strategies. Used for: DAI/USDC/USDT markets +[CappedFallbackRateSource](https://github.com/marsfoundation/sparklend-advanced/blob/master/src/CappedFallbackRateSource.sol): Wraps another rate source, caps the rate and protects against reverts with a fallback value. Used for: ETH market + ## Usage ```bash diff --git a/src/CappedFallbackRateSource.sol b/src/CappedFallbackRateSource.sol new file mode 100644 index 0000000..f386a47 --- /dev/null +++ b/src/CappedFallbackRateSource.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import { IRateSource } from "./interfaces/IRateSource.sol"; + +/** + * @title CappedFallbackRateSource + * @notice Wraps another rate source, caps the rate and protects against reverts with a fallback value. + */ +contract CappedFallbackRateSource is IRateSource { + + IRateSource public immutable source; + uint256 public immutable lowerBound; + uint256 public immutable upperBound; + uint256 public immutable defaultRate; + + constructor( + address _source, + uint256 _lowerBound, + uint256 _upperBound, + uint256 _defaultRate + ) { + require(_lowerBound <= _upperBound, "CappedFallbackRateSource/invalid-bounds"); + require(_defaultRate >= _lowerBound && _defaultRate <= _upperBound, "CappedFallbackRateSource/invalid-default-rate"); + + source = IRateSource(_source); + lowerBound = _lowerBound; + upperBound = _upperBound; + defaultRate = _defaultRate; + } + + function getAPR() external override view returns (uint256) { + try source.getAPR() returns (uint256 rate) { + if (rate < lowerBound) return lowerBound; + else if (rate > upperBound) return upperBound; + + return rate; + } catch { + return defaultRate; + } + } + + function decimals() external view override returns (uint8) { + return source.decimals(); + } + +} diff --git a/src/PotRateSource.sol b/src/PotRateSource.sol index e88b272..743577c 100644 --- a/src/PotRateSource.sol +++ b/src/PotRateSource.sol @@ -19,4 +19,8 @@ contract PotRateSource is IRateSource { return (pot.dsr() - 1e27) * 365 days; } + function decimals() external pure returns (uint8) { + return 27; + } + } diff --git a/src/RateTargetBaseInterestRateStrategy.sol b/src/RateTargetBaseInterestRateStrategy.sol index 8250cd5..70bdba8 100644 --- a/src/RateTargetBaseInterestRateStrategy.sol +++ b/src/RateTargetBaseInterestRateStrategy.sol @@ -43,12 +43,13 @@ contract RateTargetBaseInterestRateStrategy is VariableBorrowInterestRateStrateg variableRateSlope2 ) { RATE_SOURCE = IRateSource(rateSource); + require(RATE_SOURCE.decimals() <= 27, "RateTargetBaseInterestRateStrategy/invalid-rate-source-decimals"); _baseVariableBorrowRateSpread = baseVariableBorrowRateSpread; } function _getBaseVariableBorrowRate() internal override view returns (uint256) { - uint256 apr = RATE_SOURCE.getAPR(); + uint256 apr = RATE_SOURCE.getAPR() * 10 ** (27 - RATE_SOURCE.decimals()); return apr + _baseVariableBorrowRateSpread; } diff --git a/src/RateTargetKinkInterestRateStrategy.sol b/src/RateTargetKinkInterestRateStrategy.sol index 8a8cc17..b9805c6 100644 --- a/src/RateTargetKinkInterestRateStrategy.sol +++ b/src/RateTargetKinkInterestRateStrategy.sol @@ -44,6 +44,7 @@ contract RateTargetKinkInterestRateStrategy is VariableBorrowInterestRateStrateg variableRateSlope2 ) { RATE_SOURCE = IRateSource(rateSource); + require(RATE_SOURCE.decimals() <= 27, "RateTargetKinkInterestRateStrategy/invalid-rate-source-decimals"); _variableRateSlope1Spread = variableRateSlope1Spread; } @@ -51,7 +52,7 @@ contract RateTargetKinkInterestRateStrategy is VariableBorrowInterestRateStrateg function _getVariableRateSlope1() internal override view returns (uint256) { // We assume all rates are below max int. This is a reasonable assumption because // otherwise the rates will be so high that the protocol will stop working - int256 rate = int256(RATE_SOURCE.getAPR()) + _variableRateSlope1Spread - int256(_baseVariableBorrowRate); + int256 rate = int256(RATE_SOURCE.getAPR() * 10 ** (27 - RATE_SOURCE.decimals())) + _variableRateSlope1Spread - int256(_baseVariableBorrowRate); return rate > 0 ? uint256(rate) : 0; } diff --git a/src/interfaces/IRateSource.sol b/src/interfaces/IRateSource.sol index 6b5564d..85d05d8 100644 --- a/src/interfaces/IRateSource.sol +++ b/src/interfaces/IRateSource.sol @@ -3,4 +3,5 @@ pragma solidity >=0.8.0; interface IRateSource { function getAPR() external view returns (uint256); + function decimals() external view returns (uint8); } diff --git a/test/CappedFallbackRateSource.t.sol b/test/CappedFallbackRateSource.t.sol new file mode 100644 index 0000000..392afec --- /dev/null +++ b/test/CappedFallbackRateSource.t.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import { RateSourceMock } from "./mocks/RateSourceMock.sol"; + +import { CappedFallbackRateSource } from "../src/CappedFallbackRateSource.sol"; + +contract RevertingRateSource { + function getAPR() external pure returns (uint256) { + revert("RevertingRateSource/some-error"); + } +} + +contract CappedFallbackRateSourceTest is Test { + + RateSourceMock originalSource; + + CappedFallbackRateSource rateSource; + + function setUp() public { + originalSource = new RateSourceMock(0.037e18, 18); + + rateSource = new CappedFallbackRateSource({ + _source: address(originalSource), + _lowerBound: 0.01e18, + _upperBound: 0.08e18, + _defaultRate: 0.03e18 + }); + } + + function test_constructor_lowerBoundGtUpperBoundBoundary() public { + vm.expectRevert("CappedFallbackRateSource/invalid-bounds"); + new CappedFallbackRateSource({ + _source: address(originalSource), + _lowerBound: 0.01e18 + 1, // Lower bound is larger than upper bound + _upperBound: 0.01e18, + _defaultRate: 0.01e18 + }); + + new CappedFallbackRateSource({ + _source: address(originalSource), + _lowerBound: 0.01e18, + _upperBound: 0.01e18, + _defaultRate: 0.01e18 + }); + } + + function test_constructor_defaultRateLtLowerBoundBoundary() public { + vm.expectRevert("CappedFallbackRateSource/invalid-default-rate"); + new CappedFallbackRateSource({ + _source: address(originalSource), + _lowerBound: 0.01e18, + _upperBound: 0.08e18, + _defaultRate: 0.01e18 - 1 // Default rate is below lower bound + }); + + new CappedFallbackRateSource({ + _source: address(originalSource), + _lowerBound: 0.01e18, + _upperBound: 0.08e18, + _defaultRate: 0.01e18 + }); + } + + function test_constructor_defaultRateGtUpperBoundBoundary() public { + vm.expectRevert("CappedFallbackRateSource/invalid-default-rate"); + new CappedFallbackRateSource({ + _source: address(originalSource), + _lowerBound: 0.01e18, + _upperBound: 0.08e18, + _defaultRate: 0.08e18 + 1 // Default rate is above upper bound + }); + + new CappedFallbackRateSource({ + _source: address(originalSource), + _lowerBound: 0.01e18, + _upperBound: 0.08e18, + _defaultRate: 0.08e18 + }); + } + + function test_constructor() public { + assertEq(address(rateSource.source()), address(originalSource)); + assertEq(rateSource.lowerBound(), 0.01e18); + assertEq(rateSource.upperBound(), 0.08e18); + assertEq(rateSource.defaultRate(), 0.03e18); + } + + function test_decimals() public { + assertEq(rateSource.decimals(), 18); + + originalSource.setDecimals(27); + + assertEq(rateSource.decimals(), 27); + } + + function test_getAPR_rateWithinBounds() public { + assertEq(rateSource.getAPR(), 0.037e18); + } + + function test_getAPR_rateBelowLowerBoundBoundary() public { + originalSource.setRate(0.01e18 - 1); + + assertEq(rateSource.getAPR(), 0.01e18); // Use lowerBound + + originalSource.setRate(0.01e18); + + assertEq(rateSource.getAPR(), 0.01e18); // Use sourceRate + + originalSource.setRate(0.01e18 + 1); + + assertEq(rateSource.getAPR(), 0.01e18 + 1); // Use sourceRate + } + + function test_getAPR_rateAboveUpperBoundBoundary() public { + originalSource.setRate(0.08e18 + 1); + + assertEq(rateSource.getAPR(), 0.08e18); // Use upperBound + + originalSource.setRate(0.08e18); + + assertEq(rateSource.getAPR(), 0.08e18); // Use sourceRate + + originalSource.setRate(0.08e18 - 1); + + assertEq(rateSource.getAPR(), 0.08e18 - 1); // Use sourceRate + } + + function test_getAPR_rateReverts() public { + rateSource = new CappedFallbackRateSource({ + _source: address(new RevertingRateSource()), + _lowerBound: 0.01e18, + _upperBound: 0.08e18, + _defaultRate: 0.03e18 + }); + + assertEq(rateSource.getAPR(), 0.03e18); + } + +} diff --git a/test/PotRateSource.t.sol b/test/PotRateSource.t.sol index f305c1c..44edf1e 100644 --- a/test/PotRateSource.t.sol +++ b/test/PotRateSource.t.sol @@ -37,6 +37,7 @@ contract PotRateSourceTest is Test { function test_constructor() public { assertEq(address(rateSource.pot()), address(pot)); + assertEq(rateSource.decimals(), 27); } function test_bad_dsr_value() public { diff --git a/test/RateTargetBaseInterestRateStrategy.t.sol b/test/RateTargetBaseInterestRateStrategy.t.sol index 794dda1..14ebf88 100644 --- a/test/RateTargetBaseInterestRateStrategy.t.sol +++ b/test/RateTargetBaseInterestRateStrategy.t.sol @@ -17,7 +17,7 @@ contract RateTargetBaseInterestRateStrategyTest is InterestRateStrategyBaseTest RateTargetBaseInterestRateStrategy interestStrategy; function setUp() public { - rateSource = new RateSourceMock(0.05e27); + rateSource = new RateSourceMock(0.05e27, 27); interestStrategy = new RateTargetBaseInterestRateStrategy({ provider: IPoolAddressesProvider(address(123)), @@ -51,7 +51,7 @@ contract RateTargetBaseInterestRateStrategyTest is InterestRateStrategyBaseTest assertEq(interestStrategy.getBaseVariableBorrowRateSpread(), 0.005e27); } - function test_rateSource_change() public { + function test_rateSource_changeRate() public { assertEq(interestStrategy.getVariableRateSlope1(), 0.01e27); assertEq(interestStrategy.getVariableRateSlope2(), 0.45e27); assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.055e27); @@ -65,4 +65,35 @@ contract RateTargetBaseInterestRateStrategyTest is InterestRateStrategyBaseTest assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.535e27); } + function test_rateSource_changeDecimals() public { + assertEq(interestStrategy.getVariableRateSlope1(), 0.01e27); + assertEq(interestStrategy.getVariableRateSlope2(), 0.45e27); + assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.055e27); + assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.515e27); + + // Note that the rate will still convert to 27 decimals even if it's reported at 18 + rateSource.setRate(0.05e18); + rateSource.setDecimals(18); + + assertEq(interestStrategy.getVariableRateSlope1(), 0.01e27); + assertEq(interestStrategy.getVariableRateSlope2(), 0.45e27); + assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.055e27); + assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.515e27); + } + + function test_rateSource_invalidDecimals() public { + rateSource.setRate(0.07e28); + rateSource.setDecimals(28); + + vm.expectRevert("RateTargetBaseInterestRateStrategy/invalid-rate-source-decimals"); + new RateTargetBaseInterestRateStrategy({ + provider: IPoolAddressesProvider(address(123)), + rateSource: address(rateSource), + optimalUsageRatio: 0.8e27, + baseVariableBorrowRateSpread: 0.005e27, + variableRateSlope1: 0.01e27, + variableRateSlope2: 0.45e27 + }); + } + } diff --git a/test/RateTargetKinkInterestRateStrategy.t.sol b/test/RateTargetKinkInterestRateStrategy.t.sol index f06945f..9498af9 100644 --- a/test/RateTargetKinkInterestRateStrategy.t.sol +++ b/test/RateTargetKinkInterestRateStrategy.t.sol @@ -18,7 +18,7 @@ contract RateTargetKinkInterestRateStrategyTest is InterestRateStrategyBaseTest RateTargetKinkInterestRateStrategy interestStrategyPositiveSpread; function setUp() public { - rateSource = new RateSourceMock(0.05e27); + rateSource = new RateSourceMock(0.05e27, 27); interestStrategy = new RateTargetKinkInterestRateStrategy({ provider: IPoolAddressesProvider(address(123)), @@ -108,4 +108,35 @@ contract RateTargetKinkInterestRateStrategyTest is InterestRateStrategyBaseTest assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.56e27); } + function test_rateSource_changeDecimals() public { + assertEq(interestStrategy.getVariableRateSlope1(), 0.035e27); + assertEq(interestStrategy.getVariableRateSlope2(), 0.55e27); + assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.01e27); + assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.595e27); + + // Note that the rate will still convert to 27 decimals even if it's reported at 18 + rateSource.setRate(0.05e18); + rateSource.setDecimals(18); + + assertEq(interestStrategy.getVariableRateSlope1(), 0.035e27); + assertEq(interestStrategy.getVariableRateSlope2(), 0.55e27); + assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.01e27); + assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.595e27); + } + + function test_rateSource_invalidDecimals() public { + rateSource.setRate(0.07e28); + rateSource.setDecimals(28); + + vm.expectRevert("RateTargetKinkInterestRateStrategy/invalid-rate-source-decimals"); + new RateTargetKinkInterestRateStrategy({ + provider: IPoolAddressesProvider(address(123)), + rateSource: address(rateSource), + optimalUsageRatio: 0.8e27, + baseVariableBorrowRate: 0.01e27, + variableRateSlope1Spread: -0.005e27, + variableRateSlope2: 0.55e27 + }); + } + } diff --git a/test/SparkLendMainnetIntegration.t.sol b/test/SparkLendMainnetIntegration.t.sol index b825908..5a4b69d 100644 --- a/test/SparkLendMainnetIntegration.t.sol +++ b/test/SparkLendMainnetIntegration.t.sol @@ -13,6 +13,7 @@ import { IPoolConfigurator } from "sparklend-v1-core/interfaces/IPool import { IDefaultInterestRateStrategy } from "sparklend-v1-core/interfaces/IDefaultInterestRateStrategy.sol"; import { FixedPriceOracle } from "src/FixedPriceOracle.sol"; +import { CappedFallbackRateSource } from "src/CappedFallbackRateSource.sol"; import { CappedOracle } from "src/CappedOracle.sol"; import { PotRateSource } from "src/PotRateSource.sol"; import { RateTargetBaseInterestRateStrategy } from "src/RateTargetBaseInterestRateStrategy.sol"; @@ -23,6 +24,10 @@ import { WEETHExchangeRateOracle } from "src/WEETHExchangeRateOracle. import { RateSourceMock } from "./mocks/RateSourceMock.sol"; +interface ITollLike { + function kiss(address) external; +} + // TODO: Add capped oracles for WBTC (need to import the combining contract first) contract SparkLendMainnetIntegrationTest is Test { @@ -44,7 +49,7 @@ contract SparkLendMainnetIntegrationTest is Test { address RETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; address WEETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; - address ETH_IRM = 0xeCe550fB709C85CE9FC999A033447Ee2DF3ce55c; + address ETH_IRM = 0xD7A8461e6aF708a086D8285f8fD900309336347c; address USDC_ORACLE = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; address USDT_ORACLE = 0x3E7d1eAB13ad0104d2750B8863b489D65364e32D; address USDC_USDT_IRM = 0xbc8A68B0ab0617D7c90d15bb1601B25d795Dc4c8; // Note: This is the same for both because the parameters are the same @@ -52,13 +57,15 @@ contract SparkLendMainnetIntegrationTest is Test { address RETH_ORACLE = 0x05225Cd708bCa9253789C1374e4337a019e99D56; address WSTETH_ORACLE = 0x8B6851156023f4f5A66F68BEA80851c3D905Ac93; + address LST_RATE_SOURCE = 0x08669C836F41AEaD03e3EF81a59f3b8e72EC417A; + IAaveOracle aaveOracle = IAaveOracle(AAVE_ORACLE); IPoolAddressesProvider poolAddressesProvider = IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER); IPool pool = IPool(POOL); IPoolConfigurator configurator = IPoolConfigurator(POOL_CONFIGURATOR); function setUp() public { - vm.createSelectFork(getChain("mainnet").rpcUrl, 19015252); // Jan 15, 2024 + vm.createSelectFork(getChain("mainnet").rpcUrl, 19895484); // May 18, 2024 } function test_dai_market_oracle() public { @@ -85,6 +92,9 @@ contract SparkLendMainnetIntegrationTest is Test { } function test_dai_market_irm() public { + // Set fork state to before this was introduced + vm.createSelectFork(getChain("mainnet").rpcUrl, 18784436); // Dec 14, 2023 + RateTargetBaseInterestRateStrategy strategy = new RateTargetBaseInterestRateStrategy({ provider: poolAddressesProvider, @@ -135,15 +145,28 @@ contract SparkLendMainnetIntegrationTest is Test { } function test_eth_market_irm() public { - // TODO: Replace with actual ETH yield oracle when ready - uint256 mockETHYield = 0.03823984723902383709e27; // 3.8% (approx APR as of Dec 14, 2023) + CappedFallbackRateSource rateSource = new CappedFallbackRateSource({ + _source: LST_RATE_SOURCE, + _lowerBound: 0.01e18, + _upperBound: 0.08e18, + _defaultRate: 0.03e18 + }); + + // Need to whitelist the rate source + // Use a random authed address on the Chronicle oracle + vm.prank(0xc50dFeDb7E93eF7A3DacCAd7987D0960c4e2CD4b); + ITollLike(LST_RATE_SOURCE).kiss(address(rateSource)); + + uint256 ethYield = 0.028485207053926554e27; // 2.8% (approx APR as of May 18, 2024) + uint256 spread = 0.0015e27; // 0.15% + RateTargetKinkInterestRateStrategy strategy = new RateTargetKinkInterestRateStrategy({ provider: poolAddressesProvider, - rateSource: address(new RateSourceMock(mockETHYield)), + rateSource: address(rateSource), optimalUsageRatio: 0.9e27, baseVariableBorrowRate: 0, - variableRateSlope1Spread: -0.008e27, // -0.8% spread + variableRateSlope1Spread: -int256(spread), variableRateSlope2: 1.2e27 }); IDefaultInterestRateStrategy prevStrategy @@ -152,15 +175,15 @@ contract SparkLendMainnetIntegrationTest is Test { _triggerUpdate(ETH); assertEq(strategy.getBaseVariableBorrowRate(), prevStrategy.getBaseVariableBorrowRate()); - assertEq(prevStrategy.getVariableRateSlope1(), 0.032e27); - assertEq(strategy.getVariableRateSlope1(), mockETHYield - 0.008e27); + assertEq(prevStrategy.getVariableRateSlope1(), 0.028e27); + assertEq(strategy.getVariableRateSlope1(), ethYield - spread); assertEq(strategy.getVariableRateSlope2(), prevStrategy.getVariableRateSlope2()); - assertEq(prevStrategy.getMaxVariableBorrowRate(), 1.232e27); - assertEq(strategy.getMaxVariableBorrowRate(), 1.2e27 + mockETHYield - 0.008e27); + assertEq(prevStrategy.getMaxVariableBorrowRate(), 1.228e27); + assertEq(strategy.getMaxVariableBorrowRate(), 1.2e27 + ethYield - spread); _triggerUpdate(ETH); - assertEq(_getBorrowRate(ETH), 0.023148514322509339980467652e27); + assertEq(_getBorrowRate(ETH), 0.017624470144971981744160716e27); vm.prank(ADMIN); configurator.setReserveInterestRateStrategyAddress( @@ -171,7 +194,7 @@ contract SparkLendMainnetIntegrationTest is Test { _triggerUpdate(ETH); // slope1 has adjusted down a bit so the borrow rate is slightly lower at same utilization - assertEq(_getBorrowRate(ETH), 0.021875235528844931668104031e27); + assertEq(_getBorrowRate(ETH), 0.016985713431350567055736333e27); } function test_usdc_usdt_market_oracles() public { @@ -203,6 +226,9 @@ contract SparkLendMainnetIntegrationTest is Test { } function test_usdc_usdt_market_irms() public { + // Set fork state to before this was introduced + vm.createSelectFork(getChain("mainnet").rpcUrl, 19015252); // Jan 15, 2024 + RateTargetKinkInterestRateStrategy strategy = new RateTargetKinkInterestRateStrategy({ provider: poolAddressesProvider, @@ -279,6 +305,9 @@ contract SparkLendMainnetIntegrationTest is Test { } function test_reth_market_oracle() public { + // Set fork state to before this was introduced + vm.createSelectFork(getChain("mainnet").rpcUrl, 19015252); // Jan 15, 2024 + RETHExchangeRateOracle oracle = new RETHExchangeRateOracle(RETH, ETHUSD_ORACLE); // Nothing is special about this number, it just happens to be the price at this block @@ -302,6 +331,9 @@ contract SparkLendMainnetIntegrationTest is Test { } function test_wsteth_market_oracle() public { + // Set fork state to before this was introduced + vm.createSelectFork(getChain("mainnet").rpcUrl, 19015252); // Jan 15, 2024 + WSTETHExchangeRateOracle oracle = new WSTETHExchangeRateOracle(STETH, ETHUSD_ORACLE); // Nothing is special about this number, it just happens to be the price at this block @@ -343,7 +375,7 @@ contract SparkLendMainnetIntegrationTest is Test { ); // Nothing is special about this number, it just happens to be the price at this block - uint256 price = 2580.17606917e8; + uint256 price = 3225.32665359e8; assertEq(aaveOracle.getAssetPrice(WEETH), price); assertEq(aaveOracle.getSourceOfAsset(WEETH), address(oracle)); diff --git a/test/mocks/RateSourceMock.sol b/test/mocks/RateSourceMock.sol index 436620a..2fdf909 100644 --- a/test/mocks/RateSourceMock.sol +++ b/test/mocks/RateSourceMock.sol @@ -1,20 +1,28 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; -contract RateSourceMock { +import { IRateSource } from "../../src/interfaces/IRateSource.sol"; + +contract RateSourceMock is IRateSource { uint256 public rate; + uint8 public decimals; - constructor(uint256 _rate) { - rate = _rate; + constructor(uint256 _rate, uint8 _decimals) { + rate = _rate; + decimals = _decimals; } function setRate(uint256 _rate) external { rate = _rate; } - function getAPR() external view returns (uint256) { + function getAPR() external override view returns (uint256) { return rate; } + function setDecimals(uint8 _decimals) external { + decimals = _decimals; + } + }