Platform: Code4rena
Start Date: 07/09/2022
Pot Size: $20,000 CANTO
Total HM: 7
Participants: 65
Period: 1 day
Judge: 0xean
Total Solo HM: 3
Id: 159
League: ETH
Rank: 20/65
Findings: 2
Award: $146.62
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: hickuphh3
Also found by: 0xNazgul, 0xSky, CertoraInc, Deivitto, Jeiwan, SinceJuly, hansfriese, linmiaomiao, rbserver
https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-periphery.sol#L549-L593 https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-periphery.sol#L487-L522 https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-core.sol#L201-L222 https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-core.sol#L424-L437 https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-core.sol#L237-L258
When calling the following getUnderlyingPrice
function, the getPriceLP
function below can be further executed.
https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-periphery.sol#L487-L522
function getUnderlyingPrice(CToken ctoken) external override view returns(uint) { address underlying; { //manual scope to pop symbol off of stack string memory symbol = ctoken.symbol(); if (compareStrings(symbol, "cCANTO")) { underlying = address(wcanto); return getPriceNote(address(wcanto), false); } else { underlying = address(ICErc20(address(ctoken)).underlying()); // We are getting the price for a CErc20 lending market } ... if (isPair(underlying)) { // this is an LP Token return getPriceLP(IBaseV1Pair(underlying)); } ... }
When calling the following getPriceLP
function, the sample
function below, which further calls the _getAmountOut
function below, is executed to get the corresponding prices
, and the sampleReserves
function below is executed to get the corresponding assetReserves
and unitReserves
. Afterwards, token0TVL
for each value of assetReserves
and prices
is calculated by executing uint token0TVL = assetReserves[i] * (prices[i] / decimals)
. Each token0TVL
is then used to accumulate LpPricesCumulative
by executing LpPricesCumulative += (token0TVL + token1TVL) * 1e18 / supply[i]
. The LpPricesCumulative
is further used to calculate LpPrice
, which influences the return value of the getPriceLP
function in a positive relationship, by executing uint LpPrice = LpPricesCumulative / 8
.
https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-periphery.sol#L549-L593
function getPriceLP(IBaseV1Pair pair) internal view returns(uint) { uint[] memory supply = pair.sampleSupply(8, 1); uint[] memory prices; uint[] memory unitReserves; uint[] memory assetReserves; address token0 = pair.token0(); address token1 = pair.token1(); uint decimals; if (pair.stable()) { // stable pairs will be priced in terms of Note if (token0 == note) { //token0 is the unit, token1 will be priced with respect to this asset initially decimals = 10 ** (erc20(token1).decimals()); // we must normalize the price of token1 to 18 decimals prices = pair.sample(token1, decimals, 8, 1); (unitReserves, assetReserves) = pair.sampleReserves(8, 1); } else { decimals = 10 ** (erc20(token0).decimals()); prices = pair.sample(token0, decimals, 8, 1); (assetReserves, unitReserves) = pair.sampleReserves(8, 1); } } else { // non-stable pairs will be priced in terms of Canto if (token0 == address(wcanto)) { // token0 is Canto, and the unit asset of this pair is Canto decimals = 10 ** (erc20(token1).decimals()); prices = pair.sample(token1, decimals, 8, 1); (unitReserves, assetReserves) = pair.sampleReserves(8, 1); } else { decimals = 10 ** (erc20(token0)).decimals(); prices = pair.sample(token0, decimals, 8, 1); (assetReserves, unitReserves) = pair.sampleReserves(8, 1); } } uint LpPricesCumulative; for(uint i; i < 8; ++i) { uint token0TVL = assetReserves[i] * (prices[i] / decimals); uint token1TVL = unitReserves[i]; // price of the unit asset is always 1 LpPricesCumulative += (token0TVL + token1TVL) * 1e18 / supply[i]; } uint LpPrice = LpPricesCumulative / 8; // take the average of the cumulative prices if (pair.stable()) { // this asset has been priced in terms of Note return LpPrice; } // this asset has been priced in terms of Canto return LpPrice * getPriceNote(address(wcanto), false) / 1e18; // return the price in terms of Note }
https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-core.sol#L201-L222
function sample(address tokenIn, uint amountIn, uint points, uint window) public view returns (uint[] memory) { uint[] memory _prices = new uint[](points); uint lastIndex = observations.length-1; require(lastIndex >= points * window, "PAIR::NOT READY FOR PRICING"); //log if the price is requested and there are not enough observations uint i = lastIndex - (points * window); // point from which to begin the sample uint nextIndex = 0; uint index = 0; for (; i < lastIndex; i+=window) { nextIndex = i + window; uint timeElapsed = observations[nextIndex].timestamp - observations[i].timestamp; uint _reserve0 = (observations[nextIndex].reserve0Cumulative - observations[i].reserve0Cumulative) / timeElapsed; uint _reserve1 = (observations[nextIndex].reserve1Cumulative - observations[i].reserve1Cumulative) / timeElapsed; _prices[index] = _getAmountOut(amountIn, tokenIn, _reserve0, _reserve1); index = index + 1; } return _prices; }
https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-core.sol#L424-L437
function _getAmountOut(uint amountIn, address tokenIn, uint _reserve0, uint _reserve1) internal view returns (uint) { if (stable) { uint xy = _k(_reserve0, _reserve1); _reserve0 = _reserve0 * 1e18 / decimals0; _reserve1 = _reserve1 * 1e18 / decimals1; (uint reserveA, uint reserveB) = tokenIn == token0 ? (_reserve0, _reserve1) : (_reserve1, _reserve0); amountIn = tokenIn == token0 ? amountIn * 1e18 / decimals0 : amountIn * 1e18 / decimals1; uint y = reserveB - _get_y(amountIn+reserveA, xy, reserveB); return y * (tokenIn == token0 ? decimals1 : decimals0) / 1e18; } else { (uint reserveA, uint reserveB) = tokenIn == token0 ? (_reserve0, _reserve1) : (_reserve1, _reserve0); return amountIn * reserveB / (reserveA + amountIn); } }
https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-core.sol#L237-L258
function sampleReserves(uint points, uint window) public view returns (uint[] memory, uint[] memory) { uint[] memory _reserves0 = new uint[](points); uint[] memory _reserves1 = new uint[](points); uint lastIndex = observations.length-1; require(lastIndex >= points * window, "PAIR::NOT READY FOR PRICING"); uint i = lastIndex - (points * window); // point from which to begin the sample uint nextIndex = 0; uint index = 0; uint timeElapsed; for(; i < lastIndex; i+=window) { nextIndex = i + window; timeElapsed = observations[nextIndex].timestamp - observations[i].timestamp; _reserves0[index] = (observations[nextIndex].reserve0Cumulative - observations[i].reserve0Cumulative) / timeElapsed; _reserves1[index] = (observations[nextIndex].reserve1Cumulative - observations[i].reserve1Cumulative) / timeElapsed; index = index + 1; } return (_reserves0, _reserves1); }
In a situation where unitReserves[i]
is less than, equal to, or greater than but close to assetReserves[i]
when calling the getPriceLP
function, prices[i]
can be calculated to be less than decimals
after calling sample
and _getAmountOut
, which causes the corresponding token0TVL
to be 0 because uint token0TVL = assetReserves[i] * (prices[i] / decimals)
based on the current implementation. However, token0TVL
is expected to be more than 0 in this case. With token0TVL
being 0, LpPricesCumulative
and LpPrice
are less than expected, which causes the return values of the getPriceLP
and getUnderlyingPrice
functions to be less than expected as well.
Because the "Comptroller calls this [getUnderlyingPrice
] method when calculating accountLiquidities
for users who hold balances of ERC20 tokens", as stated by the documentation, an incorrect return value of getUnderlyingPrice
would lead to an incorrect accountLiquidities
. Incorrect accountLiquidities
can cause users' accounts to be subject to liquidation incorrectly, and users could lose their account assets as a result.
First, please add the following helper contract that simulates the current implementation and provides a suggested implementation for calculating token0TVL
in the getPriceLP
function.
// SPDX-License-Identifier: MIT pragma solidity 0.8.11; contract Token0TVLCalculations { // decimals_ simulates 10 ** (erc20(token1).decimals()) or 10 ** (erc20(token0).decimals()) in BaseV1Router01.getPriceLP uint256 decimals_ = 10 ** 18; // amountIn_ simulates amountIn input for BaseV1Pair.sample and BaseV1Pair._getAmountOut // note that calling BaseV1Router01.getPriceLP uses decimals as amountIn input for BaseV1Pair.sample uint256 amountIn_ = decimals_; // calculateToken0TVLLt simulates a situation where unitReserves[i] is less than assetReserves[i] when calling BaseV1Router01.getPriceLP for calculating token0TVL function calculateToken0TVLLt() external view returns (uint256, uint256) { // reserve0_ simulates _reserve0 in BaseV1Pair.sample, reserveB in BaseV1Pair._getAmountOut, _reserves0[index] in BaseV1Pair.sampleReserves, and unitReserves[i] in BaseV1Router01.getPriceLP uint256 reserve0_ = 900_000 * 10 ** 18; // reserve1_ simulates _reserve1 in BaseV1Pair.sample, reserveA in BaseV1Pair._getAmountOut, _reserves1[index] in BaseV1Pair.sampleReserves, and assetReserves[i] in BaseV1Router01.getPriceLP uint256 reserve1_ = 1_000_000 * 10 ** 18; return calculateToken0TVL(reserve0_, reserve1_); } // calculateToken0TVLEq simulates a situation where unitReserves[i] is equal to assetReserves[i] when calling BaseV1Router01.getPriceLP for calculating token0TVL function calculateToken0TVLEq() external view returns (uint256, uint256) { // reserve0_ simulates _reserve0 in BaseV1Pair.sample, reserveB in BaseV1Pair._getAmountOut, _reserves0[index] in BaseV1Pair.sampleReserves, and unitReserves[i] in BaseV1Router01.getPriceLP uint256 reserve0_ = 1_000_000 * 10 ** 18; // reserve1_ simulates _reserve1 in BaseV1Pair.sample, reserveA in BaseV1Pair._getAmountOut, _reserves1[index] in BaseV1Pair.sampleReserves, and assetReserves[i] in BaseV1Router01.getPriceLP uint256 reserve1_ = 1_000_000 * 10 ** 18; return calculateToken0TVL(reserve0_, reserve1_); } // calculateToken0TVLGc simulates a situation where unitReserves[i] is greater than but close to assetReserves[i] when calling BaseV1Router01.getPriceLP for calculating token0TVL function calculateToken0TVLGc() external view returns (uint256, uint256) { // reserve0_ simulates _reserve0 in BaseV1Pair.sample, reserveB in BaseV1Pair._getAmountOut, _reserves0[index] in BaseV1Pair.sampleReserves, and unitReserves[i] in BaseV1Router01.getPriceLP uint256 reserve0_ = 1_000_000 * 10 ** 18 + 1_000_000; // reserve1_ simulates _reserve1 in BaseV1Pair.sample, reserveA in BaseV1Pair._getAmountOut, _reserves1[index] in BaseV1Pair.sampleReserves, and assetReserves[i] in BaseV1Router01.getPriceLP uint256 reserve1_ = 1_000_000 * 10 ** 18; return calculateToken0TVL(reserve0_, reserve1_); } function calculateToken0TVL(uint256 reserve0_, uint256 reserve1_) private view returns (uint256 token0TVLCurrent, uint256 token0TVLSuggested) { // price_ simulates prices[i] in BaseV1Router01.getPriceLP // for example, the calculation for price_ simulates amountIn * reserveB / (reserveA + amountIn), which is the calculation associated with volatile pair in BaseV1Pair._getAmountOut uint256 price_ = amountIn_ * reserve0_ / (reserve1_ + amountIn_); // the calculation for token0TVLCurrent simulates assetReserves[i] * (prices[i] / decimals), which is the current implementation's calculation for token0TVL in BaseV1Router01.getPriceLP token0TVLCurrent = reserve1_ * (price_ / decimals_); // the calculation for token0TVLSuggested simulates assetReserves[i] * prices[i] / decimals, which is the suggested calculation for token0TVL that can be implemented in BaseV1Router01.getPriceLP token0TVLSuggested = reserve1_ * price_ / decimals_; } }
Then, please append the following test in the Testing LpToken Price Accuracy after large vol moves in Canto/Note pair
describe
block in test\canto\Oracle\oracle.test.ts
. This test will pass to demonstrate the described scenario.
it("token0TVL is calculated to be 0 based on current implementation when calling BaseV1Router01.getPriceLP with unitReserves[i] being less than, equal to, or greater than but close to assetReserves[i]", async () => { // the Token0TVLCalculations contract simulates the current implementation and provides a suggested implementation for calculating token0TVL in BaseV1Router01.getPriceLP const Token0TVLCalculationsFactory = await ethers.getContractFactory("Token0TVLCalculations") const token0TVLCalculations = await Token0TVLCalculationsFactory.deploy() await token0TVLCalculations.deployed() // the calculateToken0TVLLt function simulates a situation where unitReserves[i] is less than assetReserves[i] when calling BaseV1Router01.getPriceLP for calculating token0TVL // token0TVLCurrent is the token0TVL value based on the current implementation in BaseV1Router01.getPriceLP // token0TVLSuggested is the token0TVL value based on the suggested implementation that can be implemented in BaseV1Router01.getPriceLP let [token0TVLCurrent, token0TVLSuggested] = await token0TVLCalculations.calculateToken0TVLLt() // token0TVL is 0 according to the current implementation expect(Number(token0TVLCurrent)).to.be.eq(0) // yet, token0TVL is bigger than 0 according to the suggested implementation expect(Number(token0TVLSuggested)).to.be.gt(0) // the calculateToken0TVLEq function simulates a situation where unitReserves[i] is equal to assetReserves[i] when calling BaseV1Router01.getPriceLP for calculating token0TVL // token0TVLCurrent is the token0TVL value based on the current implementation in BaseV1Router01.getPriceLP // token0TVLSuggested is the token0TVL value based on the suggested implementation that can be implemented in BaseV1Router01.getPriceLP ;[token0TVLCurrent, token0TVLSuggested] = await token0TVLCalculations.calculateToken0TVLEq() // token0TVL is 0 according to the current implementation expect(Number(token0TVLCurrent)).to.be.eq(0) // yet, token0TVL is bigger than 0 according to the suggested implementation expect(Number(token0TVLSuggested)).to.be.gt(0) // the calculateToken0TVLGc function simulates a situation where unitReserves[i] is greater than but close to assetReserves[i] when calling BaseV1Router01.getPriceLP for calculating token0TVL // token0TVLCurrent is the token0TVL value based on the current implementation in BaseV1Router01.getPriceLP // token0TVLSuggested is the token0TVL value based on the suggested implementation that can be implemented in BaseV1Router01.getPriceLP ;[token0TVLCurrent, token0TVLSuggested] = await token0TVLCalculations.calculateToken0TVLGc() // token0TVL is 0 according to the current implementation expect(Number(token0TVLCurrent)).to.be.eq(0) // yet, token0TVL is bigger than 0 according to the suggested implementation expect(Number(token0TVLSuggested)).to.be.gt(0) })
VSCode
In the getPriceLP
function, https://github.com/code-423n4/2022-09-canto/blob/main/src/Swap/BaseV1-periphery.sol#L582 can be changed to the following code.
uint token0TVL = assetReserves[i] * prices[i] / decimals;
#0 - nivasan1
2022-09-08T21:26:54Z
duplicate of #41
🌟 Selected for report: lukris02
Also found by: 0x040, 0x1f8b, 0x52, 0xA5DF, 0xNazgul, 0xSky, Bnke0x0, Bronicle, CertoraInc, Chom, CodingNameKiki, Deivitto, Diraco, Dravee, EthLedger, IgnacioB, JC, JansenC, Jeiwan, R2, RaymondFam, ReyAdmirado, Rolezn, SinceJuly, TomJ, Tomo, Yiko, a12jmx, ajtra, ak1, codexploder, cryptphi, csanuragjain, erictee, fatherOfBlocks, gogo, hake, hansfriese, hickuphh3, ignacio, ontofractal, oyc_109, p_crypt0, pashov, peritoflores, rajatbeladiya, rbserver, rokinot, rvierdiiev, tnevler
242.8216 CANTO - $39.22
Currently, the number of periods requested is hardcoded to 8 and the window size is hardcoded to 1 in Swap\BaseV1-periphery.sol
. In case these numbers need to be adjusted in the future, it can be beneficial to set up admin functions for adjusting these. Please consider adding these functions for controlling state variables that represent the requested period and window sizes and using these state variables to replace the following hardcoded numbers.
Swap\BaseV1-periphery.sol 532: uint price = IBaseV1Pair(pair).quote(address(token), decimals, 8); // how much Canto is this asset worth? 544: uint price = IBaseV1Pair(pair).quote(address(token), decimals, 8); 562: (unitReserves, assetReserves) = pair.sampleReserves(8, 1); 566: (assetReserves, unitReserves) = pair.sampleReserves(8, 1); 572: (unitReserves, assetReserves) = pair.sampleReserves(8, 1); 576: (assetReserves, unitReserves) = pair.sampleReserves(8, 1); 581: for(uint i; i < 8; ++i) { 586: uint LpPrice = LpPricesCumulative / 8; // take the average of the cumulative prices
To improve readability and maintainability, constants can be used instead of magic numbers. Please consider replacing the magic numbers, such as 1e18
, used in the following code with constants.
Swap\BaseV1-core.sol 441: uint _x = x * 1e18 / decimals0; 442: uint _y = y * 1e18 / decimals1; 443: uint _a = (_x * _y) / 1e18; 444: uint _b = ((_x * _x) / 1e18 + (_y * _y) / 1e18); 445: return _a * _b / 1e18; // x3y+y3x >= k Swap\BaseV1-periphery.sol 499: return 1e18; // Stable coins supported by the lending market are instantiated by governance and their price will always be 1 note 503: return 1e18 * 1e18 / (10 ** decimals); //Scale Price as a mantissa to maintain precision in comptroller 507: return 1e18 * 1e18 / (10 ** decimals); //Scale Price as a mantissa to maintain precision in comptroller 520: return getPriceCanto(underlying) * getPriceNote(address(wcanto), false) / 1e18; 533: return price * 1e18 / decimals; //return the scaled price 545: return price * 1e18 / decimals; // divide by decimals now to maintain precision 584: LpPricesCumulative += (token0TVL + token1TVL) * 1e18 / supply[i]; 592: return LpPrice * getPriceNote(address(wcanto), false) / 1e18; // return the price in terms of Note
In the following code, underlying()
already returns an address
type. Hence, ICErc20(address(ctoken)).underlying()
does not need to be converted to address
again.
Swap\BaseV1-periphery.sol 495: underlying = address(ICErc20(address(ctoken)).underlying()); // We are getting the price for a CErc20 lending market
NatSpec comments provide rich code documentation. NatSpec comments are missing for the following functions. Please consider adding them.
Swap\BaseV1-core.sol 137: function _update(uint balance0, uint balance1, uint _reserve0, uint _reserve1, uint _totalSupply) internal { 201: function sample(address tokenIn, uint amountIn, uint points, uint window) public view returns (uint[] memory) { 224: function reserves(uint granularity) external view returns(uint, uint) { 237: function sampleReserves(uint points, uint window) public view returns (uint[] memory, uint[] memory) { 260: function totalSupplyAvg(uint granularity) external view returns(uint) { 271: function sampleSupply(uint points, uint window) public view returns (uint[] memory) { 424: function _getAmountOut(uint amountIn, address tokenIn, uint _reserve0, uint _reserve1) internal view returns (uint) { Swap\BaseV1-periphery.sol 487: function getUnderlyingPrice(CToken ctoken) external override view returns(uint) { 525: function getPriceCanto(address token_) internal view returns(uint) { 537: function getPriceNote(address token_, bool stable) internal view returns(uint) { 549: function getPriceLP(IBaseV1Pair pair) internal view returns(uint) {
Commented out code can cause confusion. If the following code are not used anymore, please remove them for readability and maintainability.
Swap\BaseV1-core.sol 420: //amountIn -= amountIn / 10000; // remove fee from amount received
Both uint
and uint256
are used in Swap\BaseV1-core.sol
and Swap\BaseV1-periphery.sol
. For explicitness and consistency, please consider using uint256
instead uint
in the following code.
Swap\BaseV1-core.sol 137: function _update(uint balance0, uint balance1, uint _reserve0, uint _reserve1, uint _totalSupply) internal { 138: uint blockTimestamp = block.timestamp; 139: uint timeElapsed = blockTimestamp - blockTimestampLast; 187: function quote(address tokenIn, uint amountIn, uint granularity) external view returns (uint amountOut) { 188: uint [] memory _prices = sample(tokenIn, amountIn, granularity, 1); 189: uint priceAverageCumulative; 190: for (uint i = 0; i < _prices.length; i++) { 202: uint[] memory _prices = new uint[](points); 204: uint lastIndex = observations.length-1; 208: uint i = lastIndex - (points * window); // point from which to begin the sample 209: uint nextIndex = 0; 210: uint index = 0; 214: uint timeElapsed = observations[nextIndex].timestamp - observations[i].timestamp; 216: uint _reserve1 = (observations[nextIndex].reserve1Cumulative - observations[i].reserve1Cumulative) / timeElapsed; 224: function reserves(uint granularity) external view returns(uint, uint) { 225: (uint[] memory _reserves0, uint[] memory _reserves1)= sampleReserves(granularity, 1); 226: uint reserveAverageCumulative0; 227: uint reserveAverageCumulative1; 229: for (uint i = 0; i < _reserves0.length; ++i) { 237: function sampleReserves(uint points, uint window) public view returns (uint[] memory, uint[] memory) { 238: uint[] memory _reserves0 = new uint[](points); 239: uint[] memory _reserves1 = new uint[](points); 241: uint lastIndex = observations.length-1; 243: uint i = lastIndex - (points * window); // point from which to begin the sample 244: uint nextIndex = 0; 245: uint index = 0; 246: uint timeElapsed; 260: function totalSupplyAvg(uint granularity) external view returns(uint) { 261: uint[] memory _totalSupplyAvg = sampleSupply(granularity, 1); 262: uint totalSupplyCumulativeAvg; 264: for (uint i = 0; i < _totalSupplyAvg.length; ++i) { 271: function sampleSupply(uint points, uint window) public view returns (uint[] memory) { 272: uint[] memory _totalSupply = new uint[](points); 274: uint lastIndex = observations.length-1; 276: uint i = lastIndex - (points * window); // point from which to begin the sample 277: uint nextIndex = 0; 278: uint index = 0; 279: uint timeElapsed; Swap\BaseV1-periphery.sol 487: function getUnderlyingPrice(CToken ctoken) external override view returns(uint) { 502: uint decimals = erc20(underlying).decimals(); 506: uint decimals = erc20(underlying).decimals(); 525: function getPriceCanto(address token_) internal view returns(uint) { 531: uint decimals = 10 ** token.decimals(); // get decimals of token 532: uint price = IBaseV1Pair(pair).quote(address(token), decimals, 8); // how much Canto is this asset worth? 537: function getPriceNote(address token_, bool stable) internal view returns(uint) { 543: uint decimals = 10 ** token.decimals(); 544: uint price = IBaseV1Pair(pair).quote(address(token), decimals, 8); 549: function getPriceLP(IBaseV1Pair pair) internal view returns(uint) { 550: uint[] memory supply = pair.sampleSupply(8, 1); 551: uint[] memory prices; 552: uint[] memory unitReserves; 553: uint[] memory assetReserves; 556: uint decimals; 579: uint LpPricesCumulative; 581: for(uint i; i < 8; ++i) { 582: uint token0TVL = assetReserves[i] * (prices[i] / decimals); 583: uint token1TVL = unitReserves[i]; // price of the unit asset is always 1 586: uint LpPrice = LpPricesCumulative / 8; // take the average of the cumulative prices
The protocol can benefit from more fixes and gas-efficient features by using a newer version of Solidity. Changes for newer Solidity versions can be found in https://blog.soliditylang.org/category/releases/. Please consider updating the versions for the following files.
Swap\BaseV1-core.sol 2: pragma solidity 0.8.11; Swap\BaseV1-libs.sol 1: pragma solidity 0.8.11; Swap\BaseV1-periphery.sol 3: pragma solidity 0.8.11;