Platform: Code4rena
Start Date: 21/12/2021
Pot Size: $30,000 USDC
Total HM: 20
Participants: 20
Period: 5 days
Judge: Jack the Pug
Total Solo HM: 13
Id: 70
League: ETH
Rank: 2/20
Findings: 6
Award: $4,325.87
π Selected for report: 6
π Solo Findings: 2
141.5133 USDC - $141.51
leastwood
The mintSynth()
function is callable by any user and creates a synthetic asset against foreignAsset
if it does not already exist. The protocol expects a user to first approve the contract as a spender before calling mintSynth()
. However, any arbitrary user could monitor the blockchain for contract approvals that match VaderPoolV2.sol
and effectively frontrun their call to mintSynth()
by setting the to
argument to their own address. As a result, the nativeDeposit
amount is transferred from the victim, and a synthetic asset is minted and finally transferred to the malicious user who is represented by the to
address.
Manual code review.
Consider removing the from
argument in mintSynth()
and update the safeTransferFrom()
call to instead utilise msg.sender
as the from
argument in transferFrom
.
#0 - jack-the-pug
2022-03-12T04:18:27Z
Dup of #147
π Selected for report: TomFrenchBlockchain
388.2396 USDC - $388.24
leastwood
_calculateVaderPrice
and _calculateUSDVPrice
are used indirectly by USDV.sol
to calculate the mint and burn amounts for the respective actions. totalUSD
and totalVader/totalUSDV
are calculated by iterating through each token pair, where the ratio between the two amounts represents the exchange rate. Each token pair is weighted according to the amount of liquidity is has, relative to other token pairs.
A token pair which is priced at a higher USD/Vader
value will impact the underlying ratio more and more as the price moves in either direction. Hence, this further reduces the impact of liquidity weightings and potentially leads to larger swings in price. This also affects how USDV
is priced.
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L191-L219
function _calculateVaderPrice( uint256[] memory liquidityWeights, uint256 totalVaderLiquidityWeight ) internal view returns (uint256) { uint256 totalUSD; uint256 totalVader; uint256 totalPairs = vaderPairs.length; for (uint256 i; i < totalPairs; ++i) { IUniswapV2Pair pair = vaderPairs[i]; ExchangePair storage pairData = twapData[address(pair)]; uint256 foreignPrice = getChainlinkPrice(pairData.foreignAsset); totalUSD += (foreignPrice * liquidityWeights[i]) / totalVaderLiquidityWeight; totalVader += (pairData .nativeTokenPriceAverage .mul(pairData.foreignUnit) .decode144() * liquidityWeights[i]) / totalVaderLiquidityWeight; } // NOTE: Accuracy of VADER & USDV is 18 decimals == 1 ether return (totalUSD * 1 ether) / totalVader; }
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L385-L413
function _calculateUSDVPrice( uint256[] memory liquidityWeights, uint256 totalUSDVLiquidityWeight ) internal view returns (uint256) { uint256 totalUSD; uint256 totalUSDV; uint256 totalPairs = usdvPairs.length; for (uint256 i; i < totalPairs; ++i) { IERC20 foreignAsset = usdvPairs[i]; ExchangePair storage pairData = twapData[address(foreignAsset)]; uint256 foreignPrice = getChainlinkPrice(address(foreignAsset)); totalUSD += (foreignPrice * liquidityWeights[i]) / totalUSDVLiquidityWeight; totalUSDV += (pairData .nativeTokenPriceAverage .mul(pairData.foreignUnit) .decode144() * liquidityWeights[i]) / totalUSDVLiquidityWeight; } // NOTE: Accuracy of VADER & USDV is 18 decimals == 1 ether return (totalUSD * 1 ether) / totalUSDV; }
Manual code review.
Consider summing the ratio between the USD
and Vader/USDV
amounts (in each iteration) when calculating the price of Vader
and USDV
. The average of this summed ratio can be taken to find an exchange rate truly weighted by liquidity and not impacted by the total price of any one token pair being tracked. This should ensure, higher priced assets such as WBTC
do not have a larger price impact when it moves against the underlying asset being tracked.
#0 - jack-the-pug
2022-03-13T14:52:38Z
Dup #42
π Selected for report: leastwood
Also found by: TomFrenchBlockchain
647.066 USDC - $647.07
leastwood
The USDV.mint
function queries the price of Vader
from the LiquidityBasedTwap
contract. The calculation to determine uAmount
in mint
is actually performed incorrectly. uAmount = (vPrice * vAmount) / 1e18;
will return the USD
amount for the provided Vader
as vPrice
is denominated in USD/Vader
. This uAmount
is subsequently used when minting tokens for the user (locked for a period of time) and fee to the contract owner.
This same issue also applies to how vAmount = (uPrice * uAmount) / 1e18;
is calculated in USDV.burn
.
This is a severe issue, as the mint
and burn
functions will always use an incorrect amount of tokens, leading to certain loss by either the protocol (if the user profits) or the user (if the user does not profit).
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/tokens/USDV.sol#L66-L98
function mint(uint256 vAmount) external onlyWhenNotLocked returns (uint256 uAmount) { uint256 vPrice = lbt.getVaderPrice(); vader.transferFrom(msg.sender, address(this), vAmount); vader.burn(vAmount); uAmount = (vPrice * vAmount) / 1e18; if (cycleTimestamp <= block.timestamp) { cycleTimestamp = block.timestamp + 24 hours; cycleMints = uAmount; } else { cycleMints += uAmount; require( cycleMints <= dailyLimit, "USDV::mint: 24 Hour Limit Reached" ); } if (exchangeFee != 0) { uint256 fee = (uAmount * exchangeFee) / _MAX_BASIS_POINTS; uAmount = uAmount - fee; _mint(owner(), fee); } _mint(address(this), uAmount); _createLock(LockTypes.USDV, uAmount); }
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/tokens/USDV.sol#L100-L120
function burn(uint256 uAmount) external onlyWhenNotLocked returns (uint256 vAmount) { uint256 uPrice = lbt.getUSDVPrice(); _burn(msg.sender, uAmount); vAmount = (uPrice * uAmount) / 1e18; if (exchangeFee != 0) { uint256 fee = (vAmount * exchangeFee) / _MAX_BASIS_POINTS; vAmount = vAmount - fee; vader.mint(owner(), fee); } vader.mint(address(this), vAmount); _createLock(LockTypes.VADER, vAmount); }
Manual code review.
Consider utilising both getVaderPrice
and getUSDVPrice
when calculating the expected uAmount
and vAmount
to mint or burn. To calculate uAmount
in mint
, vPrice
should be denominated in USDV/Vader
. To calculate vAmount
in burn
, uPrice
should be denominated in Vader/USDV
. It would be useful to add unit tests to test this explicitly as it is expected that users will interact with the USDV.sol
contract frequently.
#0 - 0xstormtrooper
2021-12-27T03:34:56Z
Mint / burn calculation with USD is intentional, modeled after LUNA / UST.
Mint USDV 1 USD worth of Vader should mint 1 USDV
Burn USDV 1 USDV should mint 1 USD worth of Vader
https://docs.terra.money/Concepts/Protocol.html#expansion-and-contraction
π Selected for report: leastwood
1437.9245 USDC - $1,437.92
leastwood
The LiquidityBasedTWAP
contract attempts to accurately track the price of VADER
and USDV
while still being resistant to flash loan manipulation and short-term volatility. The previousPrices
array is meant to track the last queried price for the two available paths, namely VADER
and USDV
.
The setupVader
function configures the VADER
token by setting previousPrices
and adding a token pair. However, syncVaderPrice
does not update previousPrices
after syncing, causing currentLiquidityEvaluation
to be dependent on the initial price for VADER
. As a result, liquidity weightings do not accurately reflect the current and most up to date price for VADER
.
This same issue also affects how USDV
calculates currentLiquidityEvaluation
.
This issue is of high risk and heavily impacts the accuracy of the TWAP implementation as the set price for VADER/USDV
diverges from current market prices. For example, as the Chainlink oracle price and initial price for VADER
diverge, currentLiquidityEvaluation
will begin to favour either on-chain or off-chain price data depending on which price result is greater. The following calculation for currentLiquidityEvaluation
outlines this behaviour.
currentLiquidityEvaluation = (reserveNative * previousPrices[uint256(Paths.VADER)]) + (reserveForeign * getChainlinkPrice(pairData.foreignAsset));
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L150-L189
function _updateVaderPrice( IUniswapV2Pair pair, ExchangePair storage pairData, uint256 timeElapsed ) internal returns (uint256 currentLiquidityEvaluation) { bool isFirst = pair.token0() == vader; (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); (uint256 reserveNative, uint256 reserveForeign) = isFirst ? (reserve0, reserve1) : (reserve1, reserve0); ( uint256 price0Cumulative, uint256 price1Cumulative, uint256 currentMeasurement ) = UniswapV2OracleLibrary.currentCumulativePrices(address(pair)); uint256 nativeTokenPriceCumulative = isFirst ? price0Cumulative : price1Cumulative; unchecked { pairData.nativeTokenPriceAverage = FixedPoint.uq112x112( uint224( (nativeTokenPriceCumulative - pairData.nativeTokenPriceCumulative) / timeElapsed ) ); } pairData.nativeTokenPriceCumulative = nativeTokenPriceCumulative; pairData.lastMeasurement = currentMeasurement; currentLiquidityEvaluation = (reserveNative * previousPrices[uint256(Paths.VADER)]) + (reserveForeign * getChainlinkPrice(pairData.foreignAsset)); }
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L221-L235
function setupVader( IUniswapV2Pair pair, IAggregatorV3 oracle, uint256 updatePeriod, uint256 vaderPrice ) external onlyOwner { require( previousPrices[uint256(Paths.VADER)] == 0, "LBTWAP::setupVader: Already Initialized" ); previousPrices[uint256(Paths.VADER)] = vaderPrice; _addVaderPair(pair, oracle, updatePeriod); }
Manual code review.
Consider updating previousPrices[uint256(Paths.VADER)]
and previousPrices[uint256(Paths.USDV)]
after syncing the respective prices for the two tokens. This will ensure the most up to date price is used when evaluating liquidity for all available token pairs.
π Selected for report: leastwood
1437.9245 USDC - $1,437.92
leastwood
The _addVaderPair
function is called by the onlyOwner
role. The relevant data in the twapData
mapping is set by querying the respective liquidity pool and Chainlink oracle. totalLiquidityWeight
for the VADER
path is also incremented by the pairLiquidityEvaluation
amount (calculated within _addVaderPair
). If a user then calls syncVaderPrice
, the recently updated totalLiquidityWeight
will be taken into consideration when iterating through all token pairs eligible for price updates to calculate the liquidity weight for each token pair. This data is stored in pastTotalLiquidityWeight
and pastLiquidityWeights
respectively.
As a result, newly added token pairs will increase pastTotalLiquidityWeight
while leaving pastLiquidityWeights
underrepresented. This only occurs if syncVaderPrice
is called before the update period for the new token has not been passed.
This issue also affects how the price for USDV
is synced.
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L299
function _addVaderPair( IUniswapV2Pair pair, IAggregatorV3 oracle, uint256 updatePeriod ) internal { require( updatePeriod != 0, "LBTWAP::addVaderPair: Incorrect Update Period" ); require(oracle.decimals() == 8, "LBTWAP::addVaderPair: Non-USD Oracle"); ExchangePair storage pairData = twapData[address(pair)]; bool isFirst = pair.token0() == vader; (address nativeAsset, address foreignAsset) = isFirst ? (pair.token0(), pair.token1()) : (pair.token1(), pair.token0()); oracles[foreignAsset] = oracle; require(nativeAsset == vader, "LBTWAP::addVaderPair: Unsupported Pair"); pairData.foreignAsset = foreignAsset; pairData.foreignUnit = uint96( 10**uint256(IERC20Metadata(foreignAsset).decimals()) ); pairData.updatePeriod = updatePeriod; pairData.lastMeasurement = block.timestamp; pairData.nativeTokenPriceCumulative = isFirst ? pair.price0CumulativeLast() : pair.price1CumulativeLast(); (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); (uint256 reserveNative, uint256 reserveForeign) = isFirst ? (reserve0, reserve1) : (reserve1, reserve0); uint256 pairLiquidityEvaluation = (reserveNative * previousPrices[uint256(Paths.VADER)]) + (reserveForeign * getChainlinkPrice(foreignAsset)); pairData.pastLiquidityEvaluation = pairLiquidityEvaluation; totalLiquidityWeight[uint256(Paths.VADER)] += pairLiquidityEvaluation; vaderPairs.push(pair); if (maxUpdateWindow < updatePeriod) maxUpdateWindow = updatePeriod; }
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L113-L148
function syncVaderPrice() public override returns ( uint256[] memory pastLiquidityWeights, uint256 pastTotalLiquidityWeight ) { uint256 _totalLiquidityWeight; uint256 totalPairs = vaderPairs.length; pastLiquidityWeights = new uint256[](totalPairs); pastTotalLiquidityWeight = totalLiquidityWeight[uint256(Paths.VADER)]; for (uint256 i; i < totalPairs; ++i) { IUniswapV2Pair pair = vaderPairs[i]; ExchangePair storage pairData = twapData[address(pair)]; uint256 timeElapsed = block.timestamp - pairData.lastMeasurement; if (timeElapsed < pairData.updatePeriod) continue; uint256 pastLiquidityEvaluation = pairData.pastLiquidityEvaluation; uint256 currentLiquidityEvaluation = _updateVaderPrice( pair, pairData, timeElapsed ); pastLiquidityWeights[i] = pastLiquidityEvaluation; pairData.pastLiquidityEvaluation = currentLiquidityEvaluation; _totalLiquidityWeight += currentLiquidityEvaluation; } totalLiquidityWeight[uint256(Paths.VADER)] = _totalLiquidityWeight; }
As shown above, pastTotalLiquidityWeight = totalLiquidityWeight[uint256(Paths.VADER)]
loads in the total liquidity weight which is updated when _addVaderPair
is called. However, pastLiquidityWeights
is calculated by iterating through each token pair that is eligible to be updated.
Manual code review.
Consider removing the line totalLiquidityWeight[uint256(Paths.VADER)] += pairLiquidityEvaluation;
in _addVaderPair
so that newly added tokens do not impact upcoming queries for VADER/USDV
price data. This should ensure syncVaderPrice
and syncUSDVPrice
cannot be manipulated when adding new tokens.
π Selected for report: leastwood
143.7924 USDC - $143.79
leastwood
The claim
function in USDV
is intended to be called when unlocking tokens previously locked after minting or burning. The claim
function does not check if i
is a valid array index, and as a result the call will revert with no relevant error message.
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/tokens/USDV.sol#L122-L142
function claim(uint256 i) external onlyWhenNotLocked returns (uint256) { Lock[] storage userLocks = locks[msg.sender]; Lock memory lock = userLocks[i]; require(lock.release <= block.timestamp, "USDV::claim: Vesting"); uint256 last = userLocks.length - 1; if (i != last) { userLocks[i] = userLocks[last]; } userLocks.pop(); if (lock.token == LockTypes.USDV) _transfer(address(this), msg.sender, lock.amount); else vader.transfer(msg.sender, lock.amount); emit LockClaimed(msg.sender, lock.token, lock.amount, lock.release); return lock.amount; }
Manual code review.
Consider adding a require
statement with a relevant error message to ensure i < userLocks.length
.
#0 - 0xstormtrooper
2021-12-27T02:32:06Z
As mentioned above, "call will revert with no relevant error message." Hence there is 0 risk.
#1 - jack-the-pug
2022-03-12T15:03:30Z
Since the index
can be reused, this can be misleading for users. I think it worth a Low
.
64.7066 USDC - $64.71
leastwood
There are currently no functions implemented to allow the onlyOwner
role to remove token pairs from the LiquidityBasedTWAP
contract. As a result, compromised tokens may severely impact the price calculations for USDV
and VADER
. The ability to add and remove tokens also helps to preserve protocol stability, further protecting users relying on price data from the LiquidityBasedTWAP
contract.
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol
Manual code review.
Consider adding a removeTokenPair
function or similar to enable this behaviour. This should help to maintain the correctness and accuracy of the TWAP implementation.
64.7066 USDC - $64.71
leastwood
The onlyOwner
role can add any arbitrary token pair as long as native asset corresponds to the VADER or USDV token. There is no check to ensure that an existing token pair is not overwritten with new pair data. Additionally, it is likely that the vaderPairs
and usdvPairs
lists will end up with duplicate token pairs which skew TWAP results in favour of the duplicated liquidity pools.
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L251-L304
function _addVaderPair( IUniswapV2Pair pair, IAggregatorV3 oracle, uint256 updatePeriod ) internal { require( updatePeriod != 0, "LBTWAP::addVaderPair: Incorrect Update Period" ); require(oracle.decimals() == 8, "LBTWAP::addVaderPair: Non-USD Oracle"); ExchangePair storage pairData = twapData[address(pair)]; bool isFirst = pair.token0() == vader; (address nativeAsset, address foreignAsset) = isFirst ? (pair.token0(), pair.token1()) : (pair.token1(), pair.token0()); oracles[foreignAsset] = oracle; require(nativeAsset == vader, "LBTWAP::addVaderPair: Unsupported Pair"); pairData.foreignAsset = foreignAsset; pairData.foreignUnit = uint96( 10**uint256(IERC20Metadata(foreignAsset).decimals()) ); pairData.updatePeriod = updatePeriod; pairData.lastMeasurement = block.timestamp; pairData.nativeTokenPriceCumulative = isFirst ? pair.price0CumulativeLast() : pair.price1CumulativeLast(); (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); (uint256 reserveNative, uint256 reserveForeign) = isFirst ? (reserve0, reserve1) : (reserve1, reserve0); uint256 pairLiquidityEvaluation = (reserveNative * previousPrices[uint256(Paths.VADER)]) + (reserveForeign * getChainlinkPrice(foreignAsset)); pairData.pastLiquidityEvaluation = pairLiquidityEvaluation; totalLiquidityWeight[uint256(Paths.VADER)] += pairLiquidityEvaluation; vaderPairs.push(pair); if (maxUpdateWindow < updatePeriod) maxUpdateWindow = updatePeriod; }
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L445-L491
function _addUSDVPair( IERC20 foreignAsset, IAggregatorV3 oracle, uint256 updatePeriod ) internal { require( updatePeriod != 0, "LBTWAP::addUSDVPair: Incorrect Update Period" ); require(oracle.decimals() == 8, "LBTWAP::addUSDVPair: Non-USD Oracle"); oracles[address(foreignAsset)] = oracle; ExchangePair storage pairData = twapData[address(foreignAsset)]; // NOTE: Redundant // pairData.foreignAsset = foreignAsset; pairData.foreignUnit = uint96( 10**uint256(IERC20Metadata(address(foreignAsset)).decimals()) ); pairData.updatePeriod = updatePeriod; pairData.lastMeasurement = block.timestamp; (uint256 nativeTokenPriceCumulative, , ) = vaderPool.cumulativePrices( foreignAsset ); pairData.nativeTokenPriceCumulative = nativeTokenPriceCumulative; (uint256 reserveNative, uint256 reserveForeign, ) = vaderPool .getReserves(foreignAsset); uint256 pairLiquidityEvaluation = (reserveNative * previousPrices[uint256(Paths.USDV)]) + (reserveForeign * getChainlinkPrice(address(foreignAsset))); pairData.pastLiquidityEvaluation = pairLiquidityEvaluation; totalLiquidityWeight[uint256(Paths.USDV)] += pairLiquidityEvaluation; usdvPairs.push(foreignAsset); if (maxUpdateWindow < updatePeriod) maxUpdateWindow = updatePeriod; }
Manual code review.
Consider preventing duplicate token pairs from being added into the TWAP implementation in LiquidityBasedTWAP.sol
. It may also be useful to add an updateTokenPair
function to enable the onlyOwner
role to change the update period and oracle.