Platform: Code4rena
Start Date: 09/11/2021
Pot Size: $75,000 USDC
Total HM: 57
Participants: 27
Period: 7 days
Judge: alcueca
Total Solo HM: 49
Id: 52
League: ETH
Rank: 4/27
Findings: 11
Award: $8,817.59
π Selected for report: 10
π Solo Findings: 7
π Selected for report: TomFrenchBlockchain
Also found by: cmichel
728.5837 USDC - $728.58
cmichel
The VaderPoolV2.mintSynth
implicitly performs a "native -> foreign" swap using VaderMath.calculateSwap(nativeDeposit,reserveNative,reserveForeign)
, the resulting amount will be minted as synths instead of transferred out as foreign tokens.
The calculateSwap
calculation, therefore, depends on the current reserves and the mintSynth
does not allow specifying any minSynthAmount
for the caller to make sure that they cannot be sandwich attacked.
It's basically doing a trade with a slippage check always set to 100%.
Any mintSynth
call can be sandwich attacked to make a profit.
A common attack in DeFi is the sandwich attack. Upon observing a trade of asset X for asset Y, an attacker frontruns the victim trade by also buying asset Y, lets the victim execute the trade, and then backruns (executes after) the victim by trading back the amount gained in the first trade. Intuitively, one uses the knowledge that someoneβs going to buy an asset, and that this trade will increase its price, to make a profit. The attackerβs plan is to buy this asset cheap, let the victim buy at an increased price, and then sell the received amount again at a higher price afterwards.
mintSynth
by performing a standard swap
, trading native assets to foreign assets, leading to a worse synth price for the victimTrades can happen at a bad price and lead to receiving fewer tokens than at a fair market price. The attacker's profit is the user's loss.
Add minimum return amount checks. Accept a function parameter that can be chosen by the transaction sender, then check that the actually received amount is above this parameter.
#0 - SamSteinGG
2021-11-25T12:25:43Z
Duplicate #2
π Selected for report: cmichel
1619.075 USDC - $1,619.07
cmichel
The 3-path hop in VaderRouter._swap
is supposed to first swap foreign assets to native assets, and then the received native assets to different foreign assets again.
The pool.swap(nativeAmountIn, foreignAmountIn)
accepts the foreign amount as the second argument.
The code however mixes these positional arguments up and tries to perform a pool0
foreign -> native swap by using the foreign amount as the native amount:
function _swap( uint256 amountIn, address[] calldata path, address to ) private returns (uint256 amountOut) { if (path.length == 3) { // ... // @audit calls this with nativeAmountIn = amountIn. but should be foreignAmountIn (second arg) return pool1.swap(0, pool0.swap(amountIn, 0, address(pool1)), to); } } // @audit should be this instead return pool1.swap(pool0.swap(0, amountIn, address(pool1)), 0, to);
All 3-path swaps through the VaderRouter
fail in the pool check when require(nativeAmountIn = amountIn <= nativeBalance - nativeReserve = 0)
is checked, as foreign amount is sent but native amount is specified.
Use return pool1.swap(pool0.swap(0, amountIn, address(pool1)), 0, to);
instead.
π Selected for report: cmichel
1619.075 USDC - $1,619.07
cmichel
The 3-path hop in VaderRouter.calculateOutGivenIn
is supposed to first swap foreign assets to native assets in pool0, and then the received native assets to different foreign assets again in pool1.
The first argument of VaderMath.calculateSwap(amountIn, reserveIn, reserveOut)
must refer to the same token as the second argument reserveIn
.
The code however mixes these positions up and first performs a swap in pool1
instead of pool0
:
function calculateOutGivenIn(uint256 amountIn, address[] calldata path) external view returns (uint256 amountOut) { if(...) { } else { return VaderMath.calculateSwap( VaderMath.calculateSwap( // @audit the inner trade should not be in pool1 for a forward swap. amountIn foreign => next param should be foreignReserve0 amountIn, nativeReserve1, foreignReserve1 ), foreignReserve0, nativeReserve0 ); } /** @audit instead should first be trading in pool0! VaderMath.calculateSwap( VaderMath.calculateSwap( amountIn, foreignReserve0, nativeReserve0 ), nativeReserve1, foreignReserve1 ); */
All 3-path swaps computations through VaderRouter.calculateOutGivenIn
will return the wrong result.
Smart contracts or off-chain scripts/frontends that rely on this value to trade will have their transaction reverted, or in the worst case lose funds.
Return the following code instead which first trades in pool0 and then in pool1:
return VaderMath.calculateSwap( VaderMath.calculateSwap( amountIn, foreignReserve0, nativeReserve0 ), nativeReserve1, foreignReserve1 );
π Selected for report: cmichel
1619.075 USDC - $1,619.07
cmichel
The TWAPOracle.registerPair
function takes in a factory
and (token0, token1)
.
The function accepts a _factory
argument which means any Uniswap-like factory can be used.
When using the actual Uniswap factory's IUniswapV2Factory(factory).getPair(token0, token1)
call, it could be that the token0
and token1
are reversed as it ignores the order.
Meaning, the price0/1CumulativeLast
could also be reversed as it matches the internal order.
The code however pushes the _pairs
assuming that the internal price0CumulativeLast, price1CumulativeLast
order matches the order of the function arguments token0, token1
.
_pairs.push( PairData({ pair: pairAddr, token0: token0, token1: token1, price0CumulativeLast: price0CumulativeLast, price1CumulativeLast: price1CumulativeLast, blockTimestampLast: blockTimestampLast, price0Average: FixedPoint.uq112x112({_x: 0}), price1Average: FixedPoint.uq112x112({_x: 0}) }) );
The prices could be inverted which leads to the oracle providing wrong prices.
It should be checked if Uniswap's internal order matches the order of the token0/1
function arguments.
If not, the cumulative prices must be swapped.
// pseudocode IUniswapV2Pair pair = IUniswapV2Pair( IUniswapV2Factory(factory).getPair(token0, token1) ); pairAddr = address(pair); price0CumulativeLast = pair.price0CumulativeLast(); price1CumulativeLast = pair.price1CumulativeLast(); (price0CumulativeLast, price1CumulativeLast) = token0 == pair.token0() ? (price0CumulativeLast, price1CumulativeLast) : (price1CumulativeLast, price0CumulativeLast);
The same issue exists in
update
#0 - SamSteinGG
2021-12-22T07:40:05Z
The TWAP oracle module has been completely removed and redesigned from scratch as LBTwap that is subject of the new audit.
π Selected for report: TomFrenchBlockchain
Also found by: cmichel
728.5837 USDC - $728.58
cmichel
The VaderPoolV2
mintFungible
and mintSynth
functions perform an unsafe nativeAsset.safeTransferFrom(from, address(this), nativeDeposit)
with a parameter-specified from
address.
Note that these functions are not called by the Router, they are directly called on the pool.
Therefore, users will usually be required to send two transactions, a first one approving the pool, and then a second one for the actual mintSynth
.
An attacker can frontrun the mintSynth(IERC20 foreignAsset, uint256 nativeDeposit, address from, address to)
function, use the same from=victim
parameter but change the to
parameter to the attacker.
It's possible to frontrun victims stealing their native token deposits and receiving synths / fungible tokens.
Remove the from
parameter and always perform the safeTransferFrom
call with from=msg.sender
.
#0 - SamSteinGG
2021-11-25T12:24:50Z
Pools are not meant to be interacted with directly as with Uniswap V2.
#1 - alcueca
2021-12-11T06:16:21Z
Duplicate of #221
π Selected for report: cmichel
cmichel
The GovernorAlpha
's council cannot veto proposals that perform a call to the contract itself.
This can be exploited by malicious proposal creators by appending a new call at the end of their proposal that simply calls an innocent function like GovernorAlpha.votingDelay()
.
The veto procedure can easily be circumvented, making the council unable to veto.
The veto check must be further restricted by specifying the actual function selector that is not allowed to be vetoed, like changeCouncil
.
#0 - SamSteinGG
2021-11-20T06:55:34Z
Duplicate of #61
#1 - alcueca
2021-12-11T04:39:49Z
Not a duplicate
π Selected for report: cmichel
485.7225 USDC - $485.72
cmichel
The LinearVesting.vestFor
function (which is called by Converter
) reverts if there already exists a vest for the user:
require( vest[user].amount == 0, "LinearVesting::selfVest: Already a vester" );
There's an attack where a griefer frontruns the vestFor
call and instead vests the smallest unit of VADER for the user
.
The original transaction will then revert and the vest will be denied
There are several ways to mitigate this. The most involved one would be to allow several separate vestings per user.
π Selected for report: cmichel
485.7225 USDC - $485.72
cmichel
The TWAPOracle.getRate
function simply performs an integer division to compute the rate.
function getRate() public view returns (uint256 result) { uint256 tUSDInUSDV = consult(USDV); uint256 tUSDInVader = consult(VADER); // @audit shouldn't this scale by 1e18 first? otherwise easily 0 result = tUSDInUSDV / tUSDInVader; }
It should first be scaled by some value, for example, 1e18
.
The rate has no decimal precision and if tUSDInVader > tUSDInUSDV
, the rate will always be zero.
The usdvtoVader
and vaderToUsdv
functions will return incorrect values.
// return as a rate with 18 decimals instead result = tUSDInUSDV * 1e18 / tUSDInVader;
#0 - SamSteinGG
2021-12-22T07:39:21Z
The TWAP oracle module has been completely removed and redesigned from scratch as LBTwap that is subject of the new audit.
π Selected for report: cmichel
485.7225 USDC - $485.72
cmichel
The TWAPOracle.consult
function is unclear to the auditor.
It seems to iterate through all registered pairs that share the token
parameter (USDV or VADER) and then sums up the foreign token pair per token
price.
And divides this sum (sumNative
) by the summed-up USD price of these foreign token pairs (sumUSD
).
I think the idea is to create some kind of average price but doing it like this does not seem to be effective because large prices are weighted a lot stronger than low prices.
Assume there are 3 USDV pairs registered: (ETH, DAI, USDC)
.
Oracle Price: USDV/ETH 4500, USDV/DAI 1, USDV/USDC 1 Pool price: USDV/ETH 4500, USDV/DAI 10, USDV/USDC 10
Even though the DAI and USDC pool prices are off by 10x, the final result is 4502/4520 = 0.996017699
very close to a price of 1.0
which seems strange.
Document how the algorithm works and make sure it's correct.
Resolve the TODO
.
#0 - SamSteinGG
2021-12-22T07:39:10Z
The TWAP oracle module has been completely removed and redesigned from scratch as LBTwap that is subject of the new audit.
30.893 USDC - $30.89
cmichel
The following code in Pausable.setPaused
can be changed to use a memory variable instead of a storage load:
if (paused) { // @audit gas, use _paused lastPauseTime = block.timestamp; }
#0 - 0xstormtrooper
2021-11-16T01:12:47Z
π Selected for report: Reigada
Also found by: Meta0xNull, cmichel
43.715 USDC - $43.72
cmichel
Some parameters of functions are not checked for non-zero values:
StakingRewards.constructor
Wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas.
Validate the parameters.
#0 - 0xstormtrooper
2021-11-16T01:11:54Z
π Selected for report: cmichel
161.9075 USDC - $161.91
cmichel
The VaderPoolV2.setTokenSupport
function allows "unsupporting" tokens, which means users might not be able to trade back their tokens in the pool anymore.
Consider disabling unsupporting tokens once they are supported.
π Selected for report: cmichel
161.9075 USDC - $161.91
cmichel
The GovernorAlpha.votingPeriod = 17280
value indicates an average block time of 15s, but a more accurate block time would be 13.2s, see blocktime.
Choose constants with an average block-time that tracks the supported network more closely.
#0 - SamSteinGG
2021-11-25T17:47:04Z
This is not a valid issue
#1 - alcueca
2021-12-11T06:14:42Z
17280 blocks is not 3 days.
// The duration of voting on a proposal, in blocks function votingPeriod() public pure virtual returns (uint256) { return 17280; // ~3 days in blocks (assuming 15s blocks) }
#2 - SamSteinGG
2021-12-16T11:52:00Z
The average block time fluctuates significantly as time moves on. The code is directly copied from Compound here. This does not constitute a low risk issue.
π Selected for report: cmichel
161.9075 USDC - $161.91
cmichel
The LinearVesting
contract does not work when using duplicate vesters
in the constructor
.
Their amount will be overwritten by the last amount but the total
tracks every single amount which leads to a discrepancy between total
and the sum of all vest[*].amount
s.
Sum up the vesting amounts per vester by doing vest[vesters[i]].amount += amounts[i];
instead of vest[vesters[i]].amount = amounts[i];
.