Skip to content

Commit

Permalink
feat: resolve vaults' underlying
Browse files Browse the repository at this point in the history
* chore: import ERC4626 interface
* lint: fix issues
* feat: resolve base asset
* gas: update snapshot
  • Loading branch information
xenide authored Jun 9, 2024
1 parent a241b6a commit 5ca6a8a
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 67 deletions.
119 changes: 60 additions & 59 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,76 +1,77 @@
FlagsLibTest:testGetDecimalDifference() (gas: 3974)
FlagsLibTest:testIsCompositeRoute() (gas: 4341)
FlagsLibTest:testPackSimplePrice(int8,uint256) (runs: 256, μ: 7794, ~: 7555)
QueryProcessorTest:testFindNearestSample_CanFindExactValue(uint32,uint256,uint256,uint256) (runs: 256, μ: 69058566, ~: 77743481)
QueryProcessorTest:testFindNearestSample_CanFindIntermediateValue(uint32,uint256,uint256,uint256) (runs: 256, μ: 72812141, ~: 81208062)
FlagsLibTest:testPackSimplePrice(int8,uint256) (runs: 256, μ: 7791, ~: 7555)
QueryProcessorTest:testFindNearestSample_CanFindExactValue(uint32,uint256,uint256,uint256) (runs: 256, μ: 65405445, ~: 73998750)
QueryProcessorTest:testFindNearestSample_CanFindIntermediateValue(uint32,uint256,uint256,uint256) (runs: 256, μ: 64633234, ~: 73958177)
QueryProcessorTest:testFindNearestSample_NotInitialized() (gas: 8937393461068805977)
QueryProcessorTest:testFindNearestSample_OneSample(uint256) (runs: 256, μ: 80315, ~: 80360)
QueryProcessorTest:testFindNearestSample_OneSample(uint256) (runs: 256, μ: 80327, ~: 80360)
QueryProcessorTest:testGetInstantValue() (gas: 124248)
QueryProcessorTest:testGetInstantValue_NotInitialized(uint256) (runs: 256, μ: 19397, ~: 19397)
QueryProcessorTest:testGetInstantValue_NotInitialized_BeyondBufferSize(uint8,uint16) (runs: 256, μ: 68389643, ~: 68389600)
QueryProcessorTest:testGetPastAccumulator_BufferEmpty(uint8) (runs: 256, μ: 27022, ~: 27087)
QueryProcessorTest:testGetPastAccumulator_ExactMatch(uint32,uint256,uint256,uint16) (runs: 256, μ: 69817768, ~: 78996938)
QueryProcessorTest:testGetPastAccumulator_ExactMatch_LatestAccumulator(uint32,uint256,uint256) (runs: 256, μ: 68901555, ~: 79087570)
QueryProcessorTest:testGetPastAccumulator_ExactMatch_OldestAccumulator(uint32,uint256,uint256) (runs: 256, μ: 68931370, ~: 79119170)
QueryProcessorTest:testGetPastAccumulator_ExtrapolatesBeyondLatest(uint32,uint256,uint256,uint256) (runs: 256, μ: 72785052, ~: 81179346)
QueryProcessorTest:testGetPastAccumulator_InterpolatesBetweenPastAccumulators(uint32,uint256,uint256,uint256) (runs: 256, μ: 72819515, ~: 81214070)
QueryProcessorTest:testGetPastAccumulator_InvalidAgo(uint32,uint256,uint256,uint256) (runs: 256, μ: 72776690, ~: 81171057)
QueryProcessorTest:testGetPastAccumulator_QueryTooOld(uint32,uint256,uint256,uint256) (runs: 256, μ: 72787880, ~: 81180941)
QueryProcessorTest:testGetTimeWeightedAverage(uint32,uint256,uint256,uint256,uint256) (runs: 256, μ: 108173406, ~: 112092926)
QueryProcessorTest:testGetInstantValue_NotInitialized_BeyondBufferSize(uint8,uint16) (runs: 256, μ: 68389672, ~: 68389600)
QueryProcessorTest:testGetPastAccumulator_BufferEmpty(uint8) (runs: 256, μ: 27030, ~: 27087)
QueryProcessorTest:testGetPastAccumulator_ExactMatch(uint32,uint256,uint256,uint16) (runs: 256, μ: 71734889, ~: 79217705)
QueryProcessorTest:testGetPastAccumulator_ExactMatch_LatestAccumulator(uint32,uint256,uint256) (runs: 256, μ: 69735120, ~: 78064819)
QueryProcessorTest:testGetPastAccumulator_ExactMatch_OldestAccumulator(uint32,uint256,uint256) (runs: 256, μ: 69765138, ~: 78096419)
QueryProcessorTest:testGetPastAccumulator_ExtrapolatesBeyondLatest(uint32,uint256,uint256,uint256) (runs: 256, μ: 64606927, ~: 73929867)
QueryProcessorTest:testGetPastAccumulator_InterpolatesBetweenPastAccumulators(uint32,uint256,uint256,uint256) (runs: 256, μ: 64641047, ~: 73964185)
QueryProcessorTest:testGetPastAccumulator_InvalidAgo(uint32,uint256,uint256,uint256) (runs: 256, μ: 64598512, ~: 73921235)
QueryProcessorTest:testGetPastAccumulator_QueryTooOld(uint32,uint256,uint256,uint256) (runs: 256, μ: 64610136, ~: 73931119)
QueryProcessorTest:testGetTimeWeightedAverage(uint32,uint256,uint256,uint256,uint256) (runs: 256, μ: 103846182, ~: 110904990)
QueryProcessorTest:testGetTimeWeightedAverage_BadSecs() (gas: 10995)
ReservoirPriceOracleTest:testClearRoute() (gas: 52231)
ReservoirPriceOracleTest:testClearRoute_AllWordsCleared() (gas: 155316)
ReservoirPriceOracleTest:testDesignatePair() (gas: 29135)
ReservoirPriceOracleTest:testDesignatePair_IncorrectPair() (gas: 21200)
ReservoirPriceOracleTest:testDesignatePair_NotOwner() (gas: 17531)
ReservoirPriceOracleTest:testDesignatePair_TokenOrderReversed() (gas: 30796)
ReservoirPriceOracleTest:testGasBountyAvailable(uint256) (runs: 256, μ: 9883, ~: 9881)
ReservoirPriceOracleTest:testClearRoute() (gas: 52319)
ReservoirPriceOracleTest:testClearRoute_AllWordsCleared() (gas: 155404)
ReservoirPriceOracleTest:testDesignatePair() (gas: 29102)
ReservoirPriceOracleTest:testDesignatePair_IncorrectPair() (gas: 21222)
ReservoirPriceOracleTest:testDesignatePair_NotOwner() (gas: 17553)
ReservoirPriceOracleTest:testDesignatePair_TokenOrderReversed() (gas: 30740)
ReservoirPriceOracleTest:testGasBountyAvailable(uint256) (runs: 256, μ: 9885, ~: 9881)
ReservoirPriceOracleTest:testGasBountyAvailable_Zero() (gas: 8939)
ReservoirPriceOracleTest:testGetLargestSafeQueryWindow() (gas: 8412)
ReservoirPriceOracleTest:testGetLatest(uint32) (runs: 256, μ: 92782, ~: 92731)
ReservoirPriceOracleTest:testGetLatest_Inverted() (gas: 96786)
ReservoirPriceOracleTest:testGetPastAccumulators() (gas: 196383)
ReservoirPriceOracleTest:testGetPastAccumulators_Inverted() (gas: 156794)
ReservoirPriceOracleTest:testGetQuote(uint256,uint256) (runs: 256, μ: 35817, ~: 35927)
ReservoirPriceOracleTest:testGetQuote_AmountInTooLarge() (gas: 13030)
ReservoirPriceOracleTest:testGetQuote_ComplicatedDecimals() (gas: 10353303)
ReservoirPriceOracleTest:testGetQuote_Inverse(uint256,uint256) (runs: 256, μ: 37978, ~: 38150)
ReservoirPriceOracleTest:testGetQuote_MultipleHops() (gas: 114521)
ReservoirPriceOracleTest:testGetQuote_MultipleHops_Inverse() (gas: 114821)
ReservoirPriceOracleTest:testGetQuote_MultipleHops_PriceZero() (gas: 127429)
ReservoirPriceOracleTest:testGetQuote_NoFallbackOracle() (gas: 13914)
ReservoirPriceOracleTest:testGetQuote_PriceZero() (gas: 16564)
ReservoirPriceOracleTest:testGetQuote_RandomizeAllParam_1HopRoute(uint256,uint256,address,address,uint8,uint8) (runs: 256, μ: 5327948, ~: 5328087)
ReservoirPriceOracleTest:testGetQuote_RandomizeAllParam_2HopRoute(uint256,uint256,uint256,address,address,address,uint8,uint8,uint8) (runs: 256, μ: 10494021, ~: 10494095)
ReservoirPriceOracleTest:testGetQuote_SameBaseQuote(uint256,address) (runs: 256, μ: 9030, ~: 9030)
ReservoirPriceOracleTest:testGetQuote_UseFallback() (gas: 35304)
ReservoirPriceOracleTest:testGetQuote_ZeroIn() (gas: 39390)
ReservoirPriceOracleTest:testGetQuotes(uint256,uint256) (runs: 256, μ: 33350, ~: 33460)
ReservoirPriceOracleTest:testGetTimeWeightedAverage() (gas: 141958)
ReservoirPriceOracleTest:testGetTimeWeightedAverage_Inverted() (gas: 121129)
ReservoirPriceOracleTest:testGetLatest(uint32) (runs: 256, μ: 92853, ~: 92787)
ReservoirPriceOracleTest:testGetLatest_Inverted() (gas: 96864)
ReservoirPriceOracleTest:testGetPastAccumulators() (gas: 196417)
ReservoirPriceOracleTest:testGetPastAccumulators_Inverted() (gas: 156850)
ReservoirPriceOracleTest:testGetQuote(uint256,uint256) (runs: 256, μ: 35789, ~: 35904)
ReservoirPriceOracleTest:testGetQuote_AmountInTooLarge() (gas: 12985)
ReservoirPriceOracleTest:testGetQuote_BaseIsVault(uint256) (runs: 256, μ: 363876, ~: 363634)
ReservoirPriceOracleTest:testGetQuote_ComplicatedDecimals() (gas: 10353280)
ReservoirPriceOracleTest:testGetQuote_Inverse(uint256,uint256) (runs: 256, μ: 37975, ~: 38148)
ReservoirPriceOracleTest:testGetQuote_MultipleHops() (gas: 114609)
ReservoirPriceOracleTest:testGetQuote_MultipleHops_Inverse() (gas: 114842)
ReservoirPriceOracleTest:testGetQuote_MultipleHops_PriceZero() (gas: 127427)
ReservoirPriceOracleTest:testGetQuote_NoFallbackOracle() (gas: 16112)
ReservoirPriceOracleTest:testGetQuote_PriceZero() (gas: 16519)
ReservoirPriceOracleTest:testGetQuote_RandomizeAllParam_1HopRoute(uint256,uint256,address,address,uint8,uint8) (runs: 256, μ: 5328071, ~: 5328117)
ReservoirPriceOracleTest:testGetQuote_RandomizeAllParam_2HopRoute(uint256,uint256,uint256,address,address,address,uint8,uint8,uint8) (runs: 256, μ: 10493941, ~: 10493960)
ReservoirPriceOracleTest:testGetQuote_SameBaseQuote(uint256,address) (runs: 256, μ: 8985, ~: 8985)
ReservoirPriceOracleTest:testGetQuote_UseFallback() (gas: 37825)
ReservoirPriceOracleTest:testGetQuote_ZeroIn() (gas: 39322)
ReservoirPriceOracleTest:testGetQuotes(uint256,uint256) (runs: 256, μ: 33345, ~: 33460)
ReservoirPriceOracleTest:testGetTimeWeightedAverage() (gas: 142014)
ReservoirPriceOracleTest:testGetTimeWeightedAverage_Inverted() (gas: 121185)
ReservoirPriceOracleTest:testSetFallbackOracle_NotOwner() (gas: 11003)
ReservoirPriceOracleTest:testSetRoute() (gas: 58892)
ReservoirPriceOracleTest:testSetRoute() (gas: 58936)
ReservoirPriceOracleTest:testSetRoute_InvalidRoute() (gas: 18049)
ReservoirPriceOracleTest:testSetRoute_InvalidRouteLength() (gas: 17655)
ReservoirPriceOracleTest:testSetRoute_MultipleHops() (gas: 196245)
ReservoirPriceOracleTest:testSetRoute_MultipleHops() (gas: 196333)
ReservoirPriceOracleTest:testSetRoute_NotSorted() (gas: 12117)
ReservoirPriceOracleTest:testSetRoute_OverwriteExisting() (gas: 162666)
ReservoirPriceOracleTest:testSetRoute_OverwriteExisting() (gas: 162732)
ReservoirPriceOracleTest:testSetRoute_SameToken() (gas: 12070)
ReservoirPriceOracleTest:testUndesignatePair() (gas: 30307)
ReservoirPriceOracleTest:testUndesignatePair_NotOwner() (gas: 15266)
ReservoirPriceOracleTest:testUpdatePriceDeviationThreshold(uint256) (runs: 256, μ: 21392, ~: 21107)
ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold() (gas: 216851)
ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold_InsufficientReward(uint256) (runs: 256, μ: 205891, ~: 206097)
ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold_ZeroRecipient() (gas: 198433)
ReservoirPriceOracleTest:testUpdatePrice_FirstUpdate() (gas: 205930)
ReservoirPriceOracleTest:testUpdatePrice_IntermediateRoutes() (gas: 15872049)
ReservoirPriceOracleTest:testUpdatePrice_PriceOutOfRange() (gas: 5355607)
ReservoirPriceOracleTest:testUpdatePrice_WithinThreshold() (gas: 206962)
ReservoirPriceOracleTest:testUpdateRewardGasAmount() (gas: 19077)
ReservoirPriceOracleTest:testUndesignatePair() (gas: 30318)
ReservoirPriceOracleTest:testUndesignatePair_NotOwner() (gas: 15288)
ReservoirPriceOracleTest:testUpdatePriceDeviationThreshold(uint256) (runs: 256, μ: 21398, ~: 21152)
ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold() (gas: 216785)
ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold_InsufficientReward(uint256) (runs: 256, μ: 205839, ~: 206053)
ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold_ZeroRecipient() (gas: 198455)
ReservoirPriceOracleTest:testUpdatePrice_FirstUpdate() (gas: 205996)
ReservoirPriceOracleTest:testUpdatePrice_IntermediateRoutes() (gas: 15872304)
ReservoirPriceOracleTest:testUpdatePrice_PriceOutOfRange() (gas: 5355619)
ReservoirPriceOracleTest:testUpdatePrice_WithinThreshold() (gas: 207028)
ReservoirPriceOracleTest:testUpdateRewardGasAmount() (gas: 19033)
ReservoirPriceOracleTest:testUpdateRewardGasAmount_NotOwner() (gas: 11006)
ReservoirPriceOracleTest:testUpdateTwapPeriod(uint256) (runs: 256, μ: 21787, ~: 21892)
ReservoirPriceOracleTest:testUpdateTwapPeriod_InvalidTwapPeriod(uint256) (runs: 256, μ: 17918, ~: 18208)
ReservoirPriceOracleTest:testWritePriceCache(uint256) (runs: 256, μ: 29981, ~: 29777)
ReservoirPriceOracleTest:testUpdateTwapPeriod(uint256) (runs: 256, μ: 21823, ~: 21892)
ReservoirPriceOracleTest:testUpdateTwapPeriod_InvalidTwapPeriod(uint256) (runs: 256, μ: 17891, ~: 18147)
ReservoirPriceOracleTest:testWritePriceCache(uint256) (runs: 256, μ: 30063, ~: 29821)
SamplesTest:testAccumulator() (gas: 3959)
SamplesTest:testAccumulator_BadVariableRequest() (gas: 3523)
SamplesTest:testInstant() (gas: 3909)
Expand Down
32 changes: 27 additions & 5 deletions src/ReservoirPriceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.0;

import { IERC20 } from "forge-std/interfaces/IERC20.sol";
import { IERC4626 } from "forge-std/interfaces/IERC4626.sol";

import { OracleErrors } from "src/libraries/OracleErrors.sol";
import {
Expand Down Expand Up @@ -34,6 +35,7 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg.
event DesignatePair(address token0, address token1, ReservoirPair pair);
event FallbackOracleSet(address fallbackOracle);
event PriceDeviationThreshold(uint256 newThreshold);
event ResolvedVaultSet(address vault, address asset);
event RewardGasAmount(uint256 newAmount);
event Route(address token0, address token1, address[] route);
event Price(address token0, address token1, uint256 price);
Expand Down Expand Up @@ -68,6 +70,9 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg.
/// @notice Designated pairs to serve as price feed for a certain token0 and token1
mapping(address token0 => mapping(address token1 => ReservoirPair pair)) public pairs;

/// @notice ERC4626 vaults resolved using internal pricing (`convertToAssets`).
mapping(address vault => address asset) public resolvedVaults;

///////////////////////////////////////////////////////////////////////////////////////////////
// CONSTRUCTOR, FALLBACKS //
///////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -381,7 +386,7 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg.
}
}

function _getQuotes(uint256 aAmount, address aBase, address aQuote, bool isGetQuotes)
function _getQuotes(uint256 aAmount, address aBase, address aQuote, bool aIsGetQuotes)
internal
view
returns (uint256 rBidOut, uint256 rAskOut)
Expand All @@ -395,11 +400,14 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg.

// route does not exist on our oracle, attempt querying the fallback
if (lRoute.length == 0) {
if (fallbackOracle == address(0)) revert OracleErrors.NoPath();
address lBaseAsset = resolvedVaults[aBase];

if (lBaseAsset != address(0)) {
uint256 lResolvedAmountIn = IERC4626(aBase).convertToAssets(aAmount);
return _getQuotes(lResolvedAmountIn, lBaseAsset, aQuote, aIsGetQuotes);
}

// We do not catch errors here so the fallback oracle will revert if it doesn't support the query.
if (isGetQuotes) (rBidOut, rAskOut) = IPriceOracle(fallbackOracle).getQuotes(aAmount, aBase, aQuote);
else rBidOut = rAskOut = IPriceOracle(fallbackOracle).getQuote(aAmount, aBase, aQuote);
return _useFallbackOracle(aAmount, aBase, aQuote, aIsGetQuotes);
} else if (lRoute.length == 2) {
if (lPrice == 0) revert OracleErrors.PriceZero();
rBidOut = rAskOut = _calcAmtOut(aAmount, lPrice, lDecimalDiff, lRoute[0] != aBase);
Expand Down Expand Up @@ -453,6 +461,14 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg.
}
}

function _useFallbackOracle(uint256 aAmount, address aBase, address aQuote, bool aIsGetQuotes) internal view returns (uint256 rBidOut, uint256 rAskOut) {
if (fallbackOracle == address(0)) revert OracleErrors.NoPath();

// We do not catch errors here so the fallback oracle will revert if it doesn't support the query.
if (aIsGetQuotes) (rBidOut, rAskOut) = IPriceOracle(fallbackOracle).getQuotes(aAmount, aBase, aQuote);
else rBidOut = rAskOut = IPriceOracle(fallbackOracle).getQuote(aAmount, aBase, aQuote);
}

///////////////////////////////////////////////////////////////////////////////////////////////
// ADMIN FUNCTIONS //
///////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -505,6 +521,12 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg.
emit SetPriceType(aType);
}

function setResolvedVault(address aVault, bool aSet) external onlyOwner {
address lAsset = aSet ? IERC4626(aVault).asset() : address(0);
resolvedVaults[aVault] = lAsset;
emit ResolvedVaultSet(aVault, lAsset);
}

/// @notice Sets the price route between aToken0 and aToken1, and also intermediate routes if previously undefined
/// @param aToken0 Address of the lower token
/// @param aToken1 Address of the higher token
Expand Down
6 changes: 3 additions & 3 deletions test/mock/MockFallbackOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ pragma solidity ^0.8.0;
import { IPriceOracle } from "src/interfaces/IPriceOracle.sol";

contract MockFallbackOracle is IPriceOracle {
function name() external view returns (string memory) {
function name() external pure returns (string memory) {
return "MOCK";
}

function getQuote(uint256 amount, address, address) external view returns (uint256 out) {
function getQuote(uint256 amount, address, address) external pure returns (uint256 out) {
out = amount;
}

function getQuotes(uint256 amount, address, address)
external
view
pure
returns (uint256 bidOut, uint256 askOut)
{
(bidOut, askOut) = (amount, amount);
Expand Down
32 changes: 32 additions & 0 deletions test/mock/StubERC4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

contract StubERC4626 {
address public asset;
uint256 private rate;
string revertMsg = "oops";

Check warning on line 7 in test/mock/StubERC4626.sol

View workflow job for this annotation

GitHub Actions / lint

Explicitly mark visibility of state
bool doRevert;

Check warning on line 8 in test/mock/StubERC4626.sol

View workflow job for this annotation

GitHub Actions / lint

Explicitly mark visibility of state

constructor(address _asset, uint256 _rate) {
asset = _asset;
rate = _rate;
}

function setRevert(bool _doRevert) external {
doRevert = _doRevert;
}

function setRate(uint256 _rate) external {
rate = _rate;
}

function convertToAssets(uint256 shares) external view returns (uint256) {
if (doRevert) revert(revertMsg);
return shares * rate / 1e18;
}

function convertToShares(uint256 assets) external view returns (uint256) {
if (doRevert) revert(revertMsg);
return assets * 1e18 / rate;
}
}
18 changes: 18 additions & 0 deletions test/unit/ReservoirPriceOracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Bytes32Lib } from "amm-core/libraries/Bytes32.sol";
import { EnumerableSetLib } from "lib/solady/src/utils/EnumerableSetLib.sol";
import { Constants } from "src/libraries/Constants.sol";
import { MockFallbackOracle } from "test/mock/MockFallbackOracle.sol";
import { StubERC4626 } from "test/mock/StubERC4626.sol";

contract ReservoirPriceOracleTest is BaseTest {
using Utils for *;
Expand Down Expand Up @@ -486,6 +487,23 @@ contract ReservoirPriceOracleTest is BaseTest {
assertGt(lAskOut, 0);
}

function testGetQuote_BaseIsVault(uint256 aRate) external {
// assume
uint256 lRate = bound(aRate, 1, 1e36);

// arrange
uint256 lAmtIn = 5e18;
StubERC4626 lVault = new StubERC4626(address(_tokenA), lRate);
_oracle.setResolvedVault(address(lVault), true);
_writePriceCache(address(_tokenA), address(_tokenB), 1e18);

// act
uint256 lAmtOut = _oracle.getQuote(lAmtIn, address(lVault), address(_tokenB));

// assert
assertEq(lAmtOut / 1e12, lAmtIn * lRate / 1e18);
}

function testUpdatePriceDeviationThreshold(uint256 aNewThreshold) external {
// assume
uint64 lNewThreshold = uint64(bound(aNewThreshold, 0, 0.1e18));
Expand Down

0 comments on commit 5ca6a8a

Please sign in to comment.