2022-10-rage-trade-judging's People
2022-10-rage-trade-judging's Issues
rvierdiiev - Share price manipulation by first depositor is possible on DnGmxJuniorVault and DnGmxSeniorVault
rvierdiiev
medium
Share price manipulation by first depositor is possible on DnGmxJuniorVault and DnGmxSeniorVault
Summary
Share price manipulation by first depositor is possible on DnGmxJuniorVault and DnGmxSeniorVault. As result next depositors might lose part of their deposited assets, while attacker will get bigger amount of assets than he deposited.
Vulnerability Detail
DnGmxSeniorVault is created and no one deposited yet.
Alice buys first share for minimum amount of USDC using DnGmxSeniorVault.deposit
function. Price of 1 share becomes 1.
Then Alice donates a big amount aliceAmount
of aave aUSDC token to DnGmxSeniorVault directly(simple ERC20 transfer). Now we have 1
amount of shares and aliceAmount + 1
of deposited asset controlled by DnGmxSeniorVault. This is how totalAssets are checked.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L370-L373
function totalAssets() public view override(IERC4626, ERC4626Upgradeable) returns (uint256 amount) {
amount = aUsdc.balanceOf(address(this));
amount += totalUsdcBorrowed();
}
Then Bob deposits arbitrary amount of assets, that is bobAmount > aliceAmount
.
As result Bob receives bobAmount / (aliceAmount + 1)
shares because of rounding here. Bob loses part of bobAmount % aliceAmount
sent to the vault, alice controls more assets in vault now.
Impact
Next depositors can lost their money, while first user will take all of them or some part.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Add limit for the first deposit to be a big amount.
Duplicate of #37
cccz - WithdrawPeriphery: withdrawToken/redeemToken allows users to withdraw other users' approved shares
cccz
high
WithdrawPeriphery: withdrawToken/redeemToken allows users to withdraw other users' approved shares
Summary
WithdrawPeriphery contract's withdrawToken/redeemToken functions allow users to use other users' addresses as from, which allows malicious users to use shares that other users have approved to WithdrawPeriphery to withdraw assets.
Vulnerability Detail
Before using the withdrawToken/redeemToken function of the WithdrawPeriphery contract, the user is required to approve the share of dnGmxJuniorVault to the WithdrawPeriphery contract, but in the withdrawToken/redeemToken function, there is no requirement that from == msg.sender, which allows malicious users to use shares approved by other users to withdraw assets.
Consider the following scenario,
User A wants to withdraw assets using the WithdrawPeriphery.withdrawToken function, he approves 1000 shares to the WithdrawPeriphery contract.
User B observes the call of the dnGmxJuniorVault.approve function, and user B calls the WithdrawPeriphery.withdrawToken function, where from is user A and receiver is user B.
Eventually, User B withdraws the asset using User A's 1000 shares.
Impact
malicious users can use shares approved by other users to withdraw assets.
Code Snippet
Tool used
Manual Review
Recommendation
Consider requiring from == msg.sender in the withdrawToken/redeemToken function of the WithdrawPeriphery contract
Duplicate of #79
8olidity - `slippageThresholdGmxBps` has no limit and can be larger than `MAX_BPS`, causing the `executeBatchStake()` to fail
8olidity
medium
slippageThresholdGmxBps
has no limit and can be larger than MAX_BPS
, causing the executeBatchStake()
to fail
Summary
slippageThresholdGmxBps
has no limit and can be larger than MAX_BPS
, causing the executeBatchStake()
to fail
Vulnerability Detail
There is no limit to how much slippageThresholdGmxBps
can be set
function setThresholds(uint256 _slippageThresholdGmxBps) external onlyOwner {
slippageThresholdGmxBps = _slippageThresholdGmxBps;
emit ThresholdsUpdated(_slippageThresholdGmxBps);
}
If the slippageThresholdGmxBps
setting is larger than MAX_BPS
, an overflow error will occur and the executeBatchStake()
will not run
function _executeVaultUserBatchStake() internal {
uint256 _roundUsdcBalance = vaultBatchingState.roundUsdcBalance;
if (_roundUsdcBalance == 0) revert NoUsdcBalance();
// use min price, because we are sending in usdc
uint256 price = gmxUnderlyingVault.getMinPrice(address(usdc));
// adjust for decimals and max possible slippage
uint256 minUsdg = _roundUsdcBalance.mulDiv(price * 1e12 * (MAX_BPS - slippageThresholdGmxBps), 1e30 * MAX_BPS);
vaultBatchingState.roundGlpStaked = _stakeGlp(address(usdc), _roundUsdcBalance, minUsdg);
emit BatchStake(vaultBatchingState.currentRound, _roundUsdcBalance, vaultBatchingState.roundGlpStaked);
}
Impact
slippageThresholdGmxBps
has no limit and can be larger than MAX_BPS
, causing the executeBatchStake()
to fail
Code Snippet
Tool used
Manual Review
Recommendation
function setThresholds(uint256 _slippageThresholdGmxBps) external onlyOwner {
require(_slippageThresholdGmxBps <=MAX_BPS );
slippageThresholdGmxBps = _slippageThresholdGmxBps;
emit ThresholdsUpdated(_slippageThresholdGmxBps);
}
ctf_sec - A malicious early user/attacker can manipulate the pricePerShare to take an unfair share of future users' deposits
ctf_sec
high
A malicious early user/attacker can manipulate the pricePerShare to take an unfair share of future users' deposits
Summary
A well known attack vector for almost all shares based liquidity pool contracts, where an early user can manipulate the price per share and profit from late users' deposits because of the precision loss caused by the rather large value of price per share.
Vulnerability Detail
A malicious early user can deposit() with 1 wei of asset token as the first depositor of the underlying token for ERC4626 vault, and get 1 wei of shares.
Then the attacker can send 10000e18 - 1 of asset tokens and inflate the price per share from 1.0000 to an extreme value of 1.0000e22 ( from (1 + 10000e18 - 1) / 1) .
As a result, the future user who deposits 19999e18 will only receive 1 wei (from 19999e18 * 1 / 10000e18) of shares token.
They will immediately lose 9999e18 or half of their deposits if they redeem() right after the deposit().
Impact
The attacker can profit from future users' deposits. While the late users will lose part of their funds to the attacker.
Code Snippet
Tool used
Manual Review
Recommendation
Consider requiring a minimal amount of share tokens to be minted for the first minter, and send a port of the initial mints as a reserve to the DAO so that the pricePerShare can be more resistant to manipulation.
Duplicate of #37
rvierdiiev - DnGmxJuniorVault.maxDeposit and DnGmxJuniorVault.afterDeposit calculate maximum assets that are allowed to deposit in different ways
rvierdiiev
medium
DnGmxJuniorVault.maxDeposit and DnGmxJuniorVault.afterDeposit calculate maximum assets that are allowed to deposit in different ways
Summary
DnGmxJuniorVault.maxDeposit and DnGmxJuniorVault.afterDeposit calculate maximum assets that are allowed to deposit in different ways.
Vulnerability Detail
This is how DnGmxJuniorVault.maxDeposit function calculates max amount of assets that are allowed to deposit.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L531-L533
function maxDeposit(address) public view override(IERC4626, ERC4626Upgradeable) returns (uint256) {
return state.depositCap - state.totalAssets(true);
}
And this is how DnGmxJuniorVault.afterDeposit function calculates max amount of assets that are allowed to deposit.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L719-L729
function afterDeposit(
uint256,
uint256,
address
) internal override {
if (totalAssets() > state.depositCap) revert DepositCapExceeded();
(uint256 currentBtc, uint256 currentEth) = state.getCurrentBorrows();
//rebalance of hedge based on assets after deposit (after deposit assets)
state.rebalanceHedge(currentBtc, currentEth, totalAssets(), false);
}
As you can see in one case it uses totalAssets()
function to get all assets.
function totalAssets() public view override(IERC4626, ERC4626Upgradeable) returns (uint256) {
return state.totalAssets();
}
And in another case it uses state.totalAssets(true)
.
This is how state.totalAssets()
and state.totalAssets(true)
are handled in DnGmxJuniorVaultManager.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L997-L1052
function totalAssets(State storage state) external view returns (uint256) {
return _totalAssets(state, false);
}
///@notice returns the total assets deposited to the vault (in glp amount)
///@param state set of all state variables of vault
///@param maximize true for maximizing the total assets value and false to minimize
///@return total asset amount (glp + usdc (in glp terms))
function totalAssets(State storage state, bool maximize) external view returns (uint256) {
return _totalAssets(state, maximize);
}
///@notice returns the total assets deposited to the vault (in glp amount)
///@param state set of all state variables of vault
///@param maximize true for maximizing the total assets value and false to minimize
///@return total asset amount (glp + usdc (in glp terms))
function _totalAssets(State storage state, bool maximize) private view returns (uint256) {
// usdc deposited by junior tranche (can be negative)
int256 dnUsdcDeposited = state.dnUsdcDeposited;
// convert int into two uints basis the sign
uint256 dnUsdcDepositedPos = dnUsdcDeposited > int256(0) ? uint256(dnUsdcDeposited) : 0;
uint256 dnUsdcDepositedNeg = dnUsdcDeposited < int256(0) ? uint256(-dnUsdcDeposited) : 0;
// add positive part to unhedgedGlp which will be added at the end
// convert usdc amount into glp amount
uint256 unhedgedGlp = (state.unhedgedGlpInUsdc + dnUsdcDepositedPos).mulDivDown(
PRICE_PRECISION,
_getGlpPrice(state, !maximize)
);
// calculate current borrow amounts
(uint256 currentBtc, uint256 currentEth) = _getCurrentBorrows(state);
uint256 totalCurrentBorrowValue = _getBorrowValue(state, currentBtc, currentEth);
// add negative part to current borrow value which will be subtracted at the end
// convert usdc amount into glp amount
uint256 borrowValueGlp = (totalCurrentBorrowValue + dnUsdcDepositedNeg).mulDivDown(
PRICE_PRECISION,
_getGlpPrice(state, !maximize)
);
// if we need to minimize then add additional slippage
if (!maximize) unhedgedGlp = unhedgedGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
if (!maximize) borrowValueGlp = borrowValueGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
// total assets considers 3 parts
// part1: glp balance in vault
// part2: glp balance in batching manager
// part3: pnl on AAVE (usdc deposit by junior tranche (i.e. dnUsdcDeposited) - current borrow value)
return
state.fsGlp.balanceOf(address(this)) +
state.batchingManager.dnGmxJuniorVaultGlpBalance() +
unhedgedGlp -
borrowValueGlp;
}
That means that one of DnGmxJuniorVault.maxDeposit and DnGmxJuniorVault.afterDeposit functions uses incorrect amount of assets.
Impact
Incorrect calculations because of incorrect assets value.
Code Snippet
Provided above.
Tool used
Manual Review
Recommendation
Use same approach to get total assets in both functions.
yixxas - Use of transferFrom() for arbitrary ERC20 tokens is not recommended
yixxas
medium
Use of transferFrom() for arbitrary ERC20 tokens is not recommended
Summary
There are plenty of non-compliant ERC20 tokens and some do not revert on failure such as ZRX. Instead, they return false and requires to be handled by the calling contract.
Vulnerability Detail
depositToken()
uses transferFrom()
to receive tokens from users. Failure of this function is not caught by the protocol if token is non-compliant.
Impact
Tokens are then staked based on amount that is transferred. If contract is holding any of such tokens, this function can be called by anyone to stake tokens that do not belong to them.
Code Snippet
Tool used
Manual Review
Recommendation
Use OpenZeppelin's safeTransferFrom()
instead.
0x0 - Third Parties May Unpause Contract
0x0
high
Third Parties May Unpause Contract
Summary
Open Zeppelin's Pausable
library enables contract administrators to prevent the operation of specific functions. This helps to protect users from loses by restricting further deposits into a set of contracts that are experiencing security/operational issues.
Vulnerability Detail
DnGmxBatchingManager.executeBatchDeposit
This function may be called by anybody. There is logic inside that will unpause the contracts if the contracts are currently paused.
Elsewhere in the contracts the ability to unpause is restricted to specific users with a modifier.
Impact
Users of the contracts can incur further loses in the following set of events:
- An attacker has exploited a vulnerability in the contract
- The administrator has paused the contract to prevent users depositing and incurring further loses
- The attacker may unpause the contract to attract and exploit more users
Code Snippet
- Example of where
_unpause()
is protected:
function unpauseDeposit() external onlyKeeper {
_unpause();
}
- Example of where anybody may call
_unpause()
:
function executeBatchDeposit() external {
// If the deposit is paused then unpause on execute batch deposit
if (paused()) _unpause();
Tool used
Manual Review
Recommendation
- Protect this function by implementing a modifier
Bnke0x0 - Collect modules can fail on zero amount transfers if assets fee is set to zero
Bnke0x0
medium
Collect modules can fail on zero amount transfers if assets fee is set to zero
Summary
Vulnerability Detail
Impact
assets fee can be zero, while collect modules do attempt to send it in such a case anyway as there is no check in place. Some ERC20 tokens do not allow zero-value transfers, reverting such attempts.
This way, a combination of zero assets fee and such a token set as a collection fee currency will revert any collect operations, rendering collect functionality unavailable
Code Snippet
'IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);'
' IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);'
References
Some ERC20 tokens revert on zero value transfers:
https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers
Tool used
Manual Review
Recommendation
Consider checking the treasury fee amount and do a transfer only when it is positive.
Now:
IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);
To be:
if (assets > 0) {
IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);
ctf_sec - Stale oracle price can be used because the oracle source is lack of price refreshness check.
ctf_sec
medium
Stale oracle price can be used because the oracle source is lack of price refreshness check.
Summary
Stale oracle price can be used.
Vulnerability Detail
The code currently uses Aave's oracle price and Aave use chainlink oracle data.
5 results - 2 files
dn-gmx-vaults\contracts\libraries\DnGmxJuniorVaultManager.sol:
1102 // AAVE oracle
1103: uint256 price = state.oracle.getAssetPrice(address(token));
1104
1150 uint256 decimals = token.decimals();
1151: uint256 price = state.oracle.getAssetPrice(address(token));
1152
1153 // @dev aave returns from same source as chainlink (which is 8 decimals)
1154: uint256 quotePrice = state.oracle.getAssetPrice(address(state.usdc));
1155
dn-gmx-vaults\contracts\vaults\DnGmxSeniorVault.sol:
314 function getPriceX128() public view returns (uint256) {
315: uint256 price = oracle.getAssetPrice(address(asset));
316
325 // use aave's oracle to get price of usdc
326: uint256 price = oracle.getAssetPrice(address(asset));
327
we are calling oracle.getAssetPrice(address(asset)) from Aave, which calls:
/// @inheritdoc IPriceOracleGetter
function getAssetPrice(address asset) public view override returns (uint256) {
AggregatorInterface source = assetsSources[asset];
if (asset == BASE_CURRENCY) {
return BASE_CURRENCY_UNIT;
} else if (address(source) == address(0)) {
return _fallbackOracle.getAssetPrice(asset);
} else {
int256 price = source.latestAnswer();
if (price > 0) {
return uint256(price);
} else {
return _fallbackOracle.getAssetPrice(asset);
}
}
}
note the line:
int256 price = source.latestAnswer();
the Aave's code does not check if the price get from the data is the updated price.
there is no freshness check. This could lead to stale prices being used.
If the market price of the token drops very quickly ("flash crashes"), and Chainlink's feed does not get updated in time, the smart contract will continue to believe the token is worth more than the market value.
Chainlink also advise developers to check for the updatedAt before using the price:
Your application should track the latestTimestamp variable or use the updatedAt value from the latestRoundData() function to make sure that the latest answer is recent enough for your application to use it. If your application detects that the reported answer is not updated within the heartbeat or within time limits that you determine are acceptable for your application, pause operation or switch to an alternate operation mode while identifying the cause of the delay.
Impact
A stale price can cause the malfunction of multiple features across the protocol:
function _getTokenPriceInUsdc(State storage state, IERC20Metadata token)
private
view
returns (uint256 scaledPrice)
{
uint256 decimals = token.decimals();
uint256 price = state.oracle.getAssetPrice(address(token));
// @dev aave returns from same source as chainlink (which is 8 decimals)
uint256 quotePrice = state.oracle.getAssetPrice(address(state.usdc));
// token price / usdc price
scaledPrice = price.mulDivDown(PRICE_PRECISION, quotePrice * 10**(decimals - 6));
}
if the function getTokenPriceInUsdc used a stale price, the function that relies on this function to calculate the optimal threshold to borrow and calculate the optimal amount to flashloan are not accurate
function _getOptimalCappedBorrows(
State storage state,
uint256 availableBorrowAmount,
uint256 usdcLiquidationThreshold
) private view returns (uint256 optimalBtcBorrow, uint256 optimalEthBorrow) {
// The value of max possible value of ETH+BTC borrow
// calculated basis available borrow amount, liqudation threshold and target health factor
// AAVE target health factor = (usdc supply value * usdc liquidation threshold)/borrow value
// whatever tokens we borrow from AAVE (ETH/BTC) we sell for usdc and deposit that usdc into AAVE
// assuming 0 slippage borrow value of tokens = usdc deposit value (this leads to very small variation in hf)
// usdc supply value = usdc borrowed from senior tranche + borrow value
// replacing usdc supply value formula above in AAVE target health factor formula
// we can replace usdc borrowed from senior tranche with available borrow amount
// we can derive max borrow value of tokens possible i.e. maxBorrowValue
uint256 maxBorrowValue = availableBorrowAmount.mulDivDown(
usdcLiquidationThreshold,
state.targetHealthFactor - usdcLiquidationThreshold
);
// calculate the borrow value of eth & btc using their weights
uint256 btcWeight = state.gmxVault.tokenWeights(address(state.wbtc));
uint256 ethWeight = state.gmxVault.tokenWeights(address(state.weth));
// get eth and btc price in usdc
uint256 btcPrice = _getTokenPriceInUsdc(state, state.wbtc);
uint256 ethPrice = _getTokenPriceInUsdc(state, state.weth);
Code Snippet
Tool used
Manual Review
Recommendation
We recommend the project fetch the price from chainlink by calling the function latestRoundData() and add the freshness check.
function latestRoundData() external view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
clems4ever - Noop rebalance under a particular condition
clems4ever
medium
Noop rebalance under a particular condition
Summary
In DnGmxJuniorVault.sol
:
If only one of BTC/ETH price changes (not both), only one side needs to be rebalanced in _rebalanceBorrow()
, and this causes the rebalance to fail because of a mistake in the condition
Vulnerability Detail
Here https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L402
if condition is true then the flashloan amounts are computed as follows:
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L409
which will trivially assign zero amounts for the flash loan in either case.
Impact
The rebalance operation is not conducted properly (and cannot be forced by admin), which means the protocol can lose funds since it is not delta neutral anymore.
Code Snippet
Tool used
Manual Review
Recommendation
change the condition to
if (btcAssetAmount != 0)
Duplicate of #62
0x0 - Non-Standard ERC20 Transfer Safety
0x0
medium
Non-Standard ERC20 Transfer Safety
Summary
There are multiple tokens deployed that do not comply with the ERC20 correctly. The use of these is not handled safely in the contracts.
Vulnerability Detail
DnGmxBatchingManager
The batching manager transfers ERC20 tokens into the contract via a depositToken()
function. This implementation does not check for the success of the transfer and continues to convert to GLP.
Impact
- If there are tokens of the same type already in the contract and the ERC20 transfer fails, these will be used to stake in GLP instead of the tokens of the calling contract.
Code Snippet
IERC20(token).transferFrom(msg.sender, address(this), amount);
Tool used
Manual Review
Recommendation
- Consider implementing the SafeERC20 wrapper from OpenZeppelin: https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#SafeERC20-safeTransferFrom-contract-IERC20-address-address-uint256-
8olidity - A normal user can call `executeBatchDeposit()` to bypass `unpauseDeposit()`
8olidity
medium
A normal user can call executeBatchDeposit()
to bypass unpauseDeposit()
Summary
A normal user can call executeBatchDeposit()
to bypass unpauseDeposit()
Vulnerability Detail
If keeper wants to suspend the contract, it can do so through pauseDeposit()
or executeBatchStake()
function executeBatchStake() external whenNotPaused onlyKeeper {
// Harvest fees prior to executing batch deposit to prevent cooldown
dnGmxJuniorVault.harvestFees();
// Convert usdc in round to sglp
_executeVaultUserBatchStake();
// To be unpaused when the staked amount is deposited
_pause();
}
function pauseDeposit() external onlyKeeper {
_pause();
}
During a contract suspension, an ordinary user can call executeBatchDeposit()
to resume the contract and get the suspended contract running
function executeBatchDeposit() external {
// If the deposit is paused then unpause on execute batch deposit
if (paused()) _unpause(); //@audit 普通用户可以将unpause
// Transfer vault glp directly, Needs to be called only for dnGmxJuniorVault
if (dnGmxJuniorVaultGlpBalance > 0) {
uint256 glpToTransfer = dnGmxJuniorVaultGlpBalance;
dnGmxJuniorVaultGlpBalance = 0;
sGlp.transfer(address(dnGmxJuniorVault), glpToTransfer);
emit VaultDeposit(glpToTransfer);
}
_executeVaultUserBatchDeposit();
}
Bypass _unpause()
restriction on keeper
function unpauseDeposit() external onlyKeeper {
_unpause();
}
Impact
A normal user can call executeBatchDeposit()
to bypass unpauseDeposit()
Code Snippet
Tool used
Manual Review
Recommendation
Restricts normal user calls to executeBatchDeposit()
Duplicate of #59
ctf_sec - Uniswap V3 conversion swap path is incorrectly hardcoded in DnGmxJuniorVaultManager.sol
ctf_sec
high
Uniswap V3 conversion swap path is incorrectly hardcoded in DnGmxJuniorVaultManager.sol
Summary
Uniswap V3 swap path is hardcoded.
Vulnerability Detail
When receiving a token from the balancer flash loan, the contract needs to perform a trade on Uniswap via two function.
///@notice swaps usdc into token
///@param state set of all state variables of vault
///@param token address of token
///@param tokenAmount token amount to be bought
///@param maxUsdcAmount maximum amount of usdc that can be sold
///@return usdcPaid amount of usdc paid for swap
///@return tokensReceived amount of tokens received on swap
function _swapUSDC(
State storage state,
address token,
uint256 tokenAmount,
uint256 maxUsdcAmount
) internal returns (uint256 usdcPaid, uint256 tokensReceived) {
ISwapRouter swapRouter = state.swapRouter;
bytes memory path = token == address(state.weth) ? USDC_TO_WETH(state) : USDC_TO_WBTC(state);
We swap swaps usdc into token.
and
///@notice swaps token into usdc
///@param state set of all state variables of vault
///@param token address of token
///@param tokenAmount token amount to be sold
///@param minUsdcAmount minimum amount of usdc required
///@return usdcReceived amount of usdc received on swap
///@return tokensUsed amount of tokens paid for swap
function _swapToken(
State storage state,
address token,
uint256 tokenAmount,
uint256 minUsdcAmount
) internal returns (uint256 usdcReceived, uint256 tokensUsed) {
ISwapRouter swapRouter = state.swapRouter;
// path of the token swap
bytes memory path = token == address(state.weth) ? WETH_TO_USDC(state) : WBTC_TO_USDC(state);
We swap token into usdc. now if we look into how WETH_TO_USDC related path construction implemented:
/* solhint-disable func-name-mixedcase */
///@notice returns usdc to weth swap path
///@param state set of all state variables of vault
///@return the path bytes
function USDC_TO_WETH(State storage state) internal view returns (bytes memory) {
return abi.encodePacked(state.weth, uint24(500), state.usdc);
}
///@notice returns usdc to wbtc swap path
///@param state set of all state variables of vault
///@return the path bytes
function USDC_TO_WBTC(State storage state) internal view returns (bytes memory) {
return abi.encodePacked(state.wbtc, uint24(3000), state.weth, uint24(500), state.usdc);
}
///@notice returns weth to usdc swap path
///@param state set of all state variables of vault
///@return the path bytes
function WETH_TO_USDC(State storage state) internal view returns (bytes memory) {
return abi.encodePacked(state.weth, uint24(500), state.usdc);
}
///@notice returns wbtc to usdc swap path
///@param state set of all state variables of vault
///@return the path bytes
function WBTC_TO_USDC(State storage state) internal view returns (bytes memory) {
return abi.encodePacked(state.wbtc, uint24(3000), state.weth, uint24(500), state.usdc);
}
clearly we can see the path is hardcoded:
in the current implementation:
USDC_TO_WETH == WETH_TO_USDC,
both use:
return abi.encodePacked(state.weth, uint24(500), state.usdc);
both only trying to construct the path from WETH to USDC.
USDT_TO_WBTC == WBTC_TO_USDC
both use:
return abi.encodePacked(state.wbtc, uint24(3000), state.weth, uint24(500), state.usdc);
both only trying to construct the path from WBTC to USDC.
Another point:
the hardcoded path may not be the optimal path:
If we see the WETH related path,
https://info.uniswap.org/#/tokens/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
the WBTC/ETH pool that has 0.05% fee has much higher trading volume than the WBTC / ETH pool that charges 0.3% fee,
but in the code, the path also assume the WBTC / ETH pool charges 0.3% fee, given the trading volume, the WBTC / ETH pool that charges 0.05% can be a better option.
Impact
The code has issue constructing path from USDC to WETH or USDC to WBTC.
The hardcoded may not have the optimal liquidity / fee charged when conducting the trade.
Code Snippet
Tool used
Manual Review
Recommendation
We recommend the project at least change from
function USDC_TO_WBTC(State storage state) internal view returns (bytes memory) {
return abi.encodePacked(state.wbtc, uint24(3000), state.weth, uint24(500), state.usdc);
}
to
function USDC_TO_WBTC(State storage state) internal view returns (bytes memory) {
return abi.encodePacked(state.usdc, uint24(500), state.weth, uint24(500), state.wbtc);
}
and
function USDC_TO_WETH(State storage state) internal view returns (bytes memory) {
return abi.encodePacked(state.weth, uint24(500), state.usdc);
}
to
function USDC_TO_WETH(State storage state) internal view returns (bytes memory) {
return abi.encodePacked(state.usdc, uint24(500), state.weth);
}
and it is better to preview the output of the different conversion paths and then use the path that has the optimal liquidity instead of hardcoding the path.
Duplicate of #73
zimu - The _totalSupply can be miscalculated in ERC4626Upgradeable.sol
zimu
medium
The _totalSupply can be miscalculated in ERC4626Upgradeable.sol
Summary
The _totalSupply can be miscalculated when an EOA with depolyed contract accidentally reenters deposit and mint function of ERC4626Upgradeable.sol twice or more.
Vulnerability Detail
It is great that the implementation of deposit and mint function of ERC4626Upgradeable.sol transfer assets before minting or ERC777s could reenter. However, the total supply _totalSupply
of the Vault's underlying asset token is calculated after deposit or mint. _totalSupply
would be calculated wrong when a depolyed asset contract with callback reenters deposit and mint twice or more.
Let's take the reentrancy of two times for example:
- An user approves and calls
ERC4626Upgradeable.deposit
to deposit his assets to the Vault; - However, In
ERC4626Upgradeable.deposit
,IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets)
, theasset
is implemented with a callback in itsERC721TokenReceiver
toERC4626Upgradeable.deposit
; - Then, 2 assets of the user may transfer to the Vault, but
_mint(receiver, shares)
inERC4626Upgradeable.deposit
only counts 1 time of shares for the user with miscalculated _totalSupply.
The calling path is as follows:
in ERC4626Upgradeable.deposit(uint256,address) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#59-71):
External calls:
- IERC20Metadata(asset).safeTransferFrom(msg.sender,address(this),assets) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#64)
State variables written after the call(s):
- _mint(receiver,shares) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#66)
- _totalSupply += amount (manual-export/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol#269)
in ERC4626Upgradeable.mint(uint256,address) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#84-95):
External calls:
- IERC20Metadata(asset).safeTransferFrom(msg.sender,address(this),assets) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#88)
State variables written after the call(s):
- _mint(receiver,shares) (dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#90)
- _totalSupply += amount (manual-export/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol#269)
Impact
If an asset
is implemented with a callback in its ERC721TokenReceiver
to ERC4626Upgradeable.deposit
or ERC4626Upgradeable.mint
, the shares of user would be miscalculated with wrong _totalSupply
.
Code Snippet
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L59-L71
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L84-L95
Tool used
Manual Review
Recommendation
To implement another function independently scans, calculates and updates the shares of users of the Vault’s underlying asset token.
joestakey - `DnGmxSeniorVault` share minting can be broken by early depositor.
joestakey
high
DnGmxSeniorVault
share minting can be broken by early depositor.
Summary
An early minter can break the DnGmxSeniorVault
share price, resulting in future depositors losing USDC
upon withdrawal.
Vulnerability Detail
DnGmxSeniorVault
allows users to deposit USDC that can serve as collateral on Aave, while earning the Aave Supply APR as well as a portion of ETH Rewards from GMX based on the utilisation ratio.
Users can deposit USDC
by calling DnGmxSeniorVault.deposit()
. The function calls ERC4626Upgradeable.deposit()
, which computes the amount of shares to be minted, and transfers the USDC
to the Senior Tranche.
The issue is that because of how convertToShares
computes the amount of shares to be minted, an early minter can inflate the share price and steal USDC
from future depositors:
- Alice calls
DnGmxSeniorVault.deposit(1)
, depositing1
unit ofUsdc
in the Senior Tranche. She receives1
share. - Alice transfers
1e6 - 1
USDC
to the vault using theERC20.transfer()
method. - Bob calls
DnGmxSeniorVault.deposit(1.999999e6)
. - Because of Alice's transfer,
aUsdc.balanceOf(vault) = 1e6
.1 * 1.9999 e6 / 1e6
rounds to1
: Bob receives1
share: the same amount as Alice. - Alice calls
DnGmxSeniorVault.withdraw
to redeem her shares: because she owns half of the shares, she will receive ~1.5 * 1e6
aUSDC
, effectively stealing approximately0.5 * 1e6
aUSDC
from Bob.
Impact
Early minters can essentially steal USDC
from future minters
Code Snippet
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L195
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L371
Tool used
Manual Review
Recommendation
Consider sending the first 1000 shares to the address 0, a mitigation used in Uniswap V2.
Duplicate of #37
0xmuxyz - `safeTransferFrom()` function should be used instead of `transferFrom()` function
0xmuxyz
medium
safeTransferFrom()
function should be used instead of transferFrom()
function
Summary
safeTransferFrom()
function should be used instead oftransferFrom()
function
Vulnerability Detail
transferFrom()
function is used in the following lines.- However,
transferFrom()
function does not return whether transferring tokens is successful or not.
- However,
Impact
- This vulnerability lead to unexpected-behavior that a transaction could continue to proceed even if transferring token using
transferFrom()
function fail. As a result, for example, unexpected-values may be stored even if transferring tokens fail.
Code Snippet
transferFrom()
is used in several parts in this repo:
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L187
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L202
Tool used
- Manual Review
Recommendation
- Should use
safeTransferFrom()
instead of usingtransferFrom()
- The benefit of using
safeTransferFrom()
is that the boolean return value isautomatically asserted
. If a token returns false on transfer or any other operation, a contract usingsafeTransferFrom()
willrevert
. This is the easiest way to check whether transferring tokens is successful or not.
https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#SafeERC20
- The benefit of using
simon135 - An attacker can give weth to the contract and can send a little weth and cause the else statement to get caused increasing the seniorVaultWethRewards or deceasing and making it zero
simon135
high
An attacker can give weth to the contract and can send a little weth and cause the else statement to get caused increasing the seniorVaultWethRewards or deceasing and making it zero
Summary
An attacker can send weth to the contract that can be small enough for the function to revert or be too much and not go into the right-if statement causing a loss of funds
Vulnerability Detail
2 scenarios:
1.
since the attacker transferred weth to the contract the harvest won't happen and the attacker can do this at every harvest call and make sure nobody gets the harvest.
or not hit the first if statement which balances glp through the batch contract which doesn't happen to cause glp to be not balanced causing liquidations to happen because glp is not balanced and if other tokens aren't doing well. Also, the assumption is broken because it's not evenly balanced.
2. There is not enough weth and if the protocol fee is more than the weth in the contract it will revert. So on the first few harvests, the function will revert which can cause the holders of the strategy not to get the harvest amount which then there Are no incentives to keep funds in.
Impact
loss of funds and loss of incentives for users
Code Snippet
// total weth harvested which is not compounded
// its possible that this is accumulated value over multiple rebalance if in all of those it was below threshold
uint256 wethHarvested = state.weth.balanceOf(address(this)) - state.protocolFee - state.seniorVaultWethRewards;
if (wethHarvested > state.wethConversionThreshold) {
// weth harvested > conversion threshold
uint256 protocolFeeHarvested = (wethHarvested * state.feeBps) / MAX_BPS;
// protocol fee incremented
state.protocolFee += protocolFeeHarvested;
// protocol fee to be kept in weth
// remaining amount needs to be compounded
uint256 wethToCompound = wethHarvested - protocolFeeHarvested;
// share of the wethToCompound that belongs to senior tranche
uint256 dnGmxSeniorVaultWethShare = state.dnGmxSeniorVault.getEthRewardsSplitRate().mulDivDown(
wethToCompound,
FeeSplitStrategy.RATE_PRECISION
);
// share of the wethToCompound that belongs to junior tranche
uint256 dnGmxWethShare = wethToCompound - dnGmxSeniorVaultWethShare;
// total senior tranche weth which is not compounded
uint256 _seniorVaultWethRewards = state.seniorVaultWethRewards + dnGmxSeniorVaultWethShare;
uint256 glpReceived;
{
// converts junior tranche share of weth into glp using batching manager
// we need to use batching manager since there is a cooldown period on sGLP
// if deposited directly for next 15mins withdrawals would fail
uint256 price = state.gmxVault.getMinPrice(address(state.weth));
uint256 usdgAmount = dnGmxWethShare.mulDivDown(
price * (MAX_BPS - state.slippageThresholdGmxBps),
PRICE_PRECISION * MAX_BPS
);
Tool used
Manual Review
Recommendation
send a little weth to the contract or find to only allow harvest after a certain time and put access control on the function
Bnke0x0 - ERC4626 does not work with fee-on-transfer tokens
Bnke0x0
medium
ERC4626 does not work with fee-on-transfer tokens
Summary
Vulnerability Detail
Impact
The ERC4626Upgradeable.deposit/mint functions do not work well with fee-on-transfer tokens as the assets variable is the pre-fee amount, including the fee, whereas the totalAssets do not include the fee anymore.
Code Snippet
This can be abused to mint more shares than desired.
' function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) {
// Check for rounding error since we round down in previewDeposit.
require((shares = previewDeposit(assets)) != 0, 'ZERO_SHARES');
// Need to transfer before minting or ERC777s could reenter.
IERC20Metadata(asset).safeTransferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
afterDeposit(assets, shares, receiver);
}'
Tool used
Manual Review
Recommendation
assets should be the amount excluding the fee, i.e., the amount the contract actually received.
This can be done by subtracting the pre-contract balance from the post-contract balance.
However, this would create another issue with ERC777 tokens.
Maybe previewDeposit
should be overwritten by vaults supporting fee-on-transfer tokens to predict the post-fee assets. And do the shares computation on that, but then the afterDeposit
is still called with the original assets and implementers need to be aware of this.
0x0 - Vaults Could Be Left Without An Owner
0x0
medium
Vaults Could Be Left Without An Owner
Summary
The vaults use OwnableUpgradeable
from Open Zeppelin to manage ownership of each deployed vault. There's potential for the owner to accidentally renounce ownership and for the contracts to be left without an owner.
Vulnerability Detail
These vaults all import OwnableUpgradeable
. This library has a renounceOwnership()
function which if accidentally called by the owner will render the vault without ownership.
Impact
- The owner could accidentally leave the contract without ownership.
Code Snippet
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
Tool used
Manual Review
Recommendation
- Override this function to prevent accidental contract ownership loss:
function renounceOwnership() public override onlyOwner {
revert();
}
8olidity - approve(0) first
8olidity
medium
approve(0) first
Summary
approve(0) first
Vulnerability Detail
For some special tokens, such as usdt, you set the value of approve to 0 before setting it to another value
// dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol
function _stakeGlp(
address token,
uint256 amount,
uint256 minUSDG
) internal returns (uint256 glpStaked) {
// swap token to obtain sGLP
IERC20(token).approve(address(glpManager), amount); // @audit approve(0)
// will revert if notional output is less than minUSDG
glpStaked = rewardRouter.mintAndStakeGlp(token, amount, minUSDG, 0);
}
// dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol
function initialize(
address _usdc,
string calldata _name,
string calldata _symbol,
address _poolAddressesProvider
) external initializer {
IERC20(asset).approve(address(pool), type(uint256).max);
}
function grantAllowances() external onlyOwner {
// allow aave lending pool to spend asset
IERC20(asset).approve(aavePool, type(uint256).max);
}
Impact
approve(0) first
Code Snippet
Tool used
Manual Review
Recommendation
approve(0) first
Bnke0x0 - _stakeGlp, grantAllowances and setAddresses functions ERC20 missing return value check
Bnke0x0
medium
_stakeGlp, grantAllowances and setAddresses functions ERC20 missing return value check
Summary
Vulnerability Detail
_stakeGlp, grantAllowances and setAddresses functions performs an ERC20.approve() call but does not check the success return value. Some tokens do not revert if the approval failed but return false instead.
Impact
Tokens that don't actually perform the approve and return false are still counted as correct approve.
Code Snippet
'IERC20(token).approve(address(glpManager), amount);'
'IERC20(asset).approve(address(pool), type(uint256).max);'
'IERC20(asset).approve(aavePool, type(uint256).max);'
Tool used
Manual Review
Recommendation
I recommend using OpenZeppelin’s SafeERC20 versions with the safeApprove function that handles the return value check as well as non-standard-compliant tokens.
8olidity - Protocol does not work with fee-on-transfer tokens
8olidity
medium
Protocol does not work with fee-on-transfer tokens
Summary
Fee-on-transfer tokens are not supported by the protocol.
Vulnerability Detail
In the depositToken()
function, the user sends the amount of tokens to the address of the contract, but the contract cannot receive the amount of tokens if the token is a token that charges a fee
function depositToken(
address token,
uint256 amount,
uint256 minUSDG
) external whenNotPaused onlyDnGmxJuniorVault returns (uint256 glpStaked) {
// revert for zero values
if (token == address(0)) revert InvalidInput(0x30);
if (amount == 0) revert InvalidInput(0x31);
// dnGmxJuniorVault gives approval to batching manager to spend token
IERC20(token).transferFrom(msg.sender, address(this), amount); //@audit use safetransferfrom()
// convert tokens to glp
glpStaked = _stakeGlp(token, amount, minUSDG);
dnGmxJuniorVaultGlpBalance += glpStaked.toUint128();
emit DepositToken(0, token, msg.sender, amount, glpStaked);
}
Impact
Protocol does not work with fee-on-transfer tokens
Code Snippet
Tool used
Manual Review
Recommendation
Use a token whitelist
rvierdiiev - Aave price oracle can be changed
rvierdiiev
medium
Aave price oracle can be changed
Summary
Aave price oracle can be changed in PoolAddressesProvider
, but the protocol will continue use old one as it sets it on initialization instead of fetching from PoolAddressesProvider
every time.
Vulnerability Detail
The protocol uses aave price oracle to get token prices.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/libraries/DnGmxJuniorVaultManager.sol#L1099-L1107
function _getTokenPrice(State storage state, IERC20Metadata token) private view returns (uint256) {
uint256 decimals = token.decimals();
// AAVE oracle
uint256 price = state.oracle.getAssetPrice(address(token));
// @dev aave returns from same source as chainlink (which is 8 decimals)
return price.mulDivDown(PRICE_PRECISION, 10**(decimals + 2));
}
For DnGmxJuniorVault state.oracle
value is stored in 2 methods. In initialize
and in setHedgeParams
.
In both cases oracle address is fetched using poolAddressProvider.getPriceOracle()
. Once it's set, then oracle address is not updating and protocol using it for calculations.
But aave PoolAddressesProvider
has a setter method to change price oracle.
https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/configuration/PoolAddressesProvider.sol#L105-L109
function setPriceOracle(address newPriceOracle) external override onlyOwner {
address oldPriceOracle = _addresses[PRICE_ORACLE];
_addresses[PRICE_ORACLE] = newPriceOracle;
emit PriceOracleUpdated(oldPriceOracle, newPriceOracle);
}
That means that in any time owner of PoolAddressesProvider can change price oracle, while the protocol will continue using old oracle. This can lead to unpredictable results from simple reverting on calls or returning outdated values.
In case when prices will be incorrect, protocol will have calculations problems.
Also from docs of aave
https://docs.aave.com/developers/v/1.0/developing-on-aave/the-protocol/price-oracle
Always get the latest price oracle address by calling getPriceOracle() on the LendingPoolAddressProvider contract.
I believe that the same should be considered for PoolAddressesProvider contract as well.
Impact
Protocol will use wrong prices and calculations will be incorrect.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Fetch poolAddressProvider.getPriceOracle()
address everytime or at least once in some duration.
ctf_sec - Pause function also pause withdraw / redeem, locking user's fund.
ctf_sec
medium
Pause function also pause withdraw / redeem, locking user's fund.
Summary
Pause function also pause withdraw / redeem, locking user's fund.
Vulnerability Detail
The admin can pause the contract, but pausing the contract block the user's withdraw/redeem request in both the junior vault and senior vault.
Impact
The user's funds are locked when the admin pauses the contract.
Code Snippet
Tool used
Manual Review
Recommendation
We recommend the project not block the user's withdraw / redeem request and remove whenNotPaused modifier from the vault redeem / withdraw function.
Nyx - Use safeTransfer/safeTransferFrom instead of transfer/transferFrom
Nyx
medium
Use safeTransfer/safeTransferFrom instead of transfer/transferFrom
Summary
Vulnerability Detail
Some tokens (like USDT) don't correctly implement the EIP20 standard and their transfer/ transferFrom function return void instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert.
The ERC20.transfer() and ERC20.transferFrom() functions return a boolean value indicating success. This parameter needs to be checked for success. Some tokens do not revert if the transfer failed but return false instead.
Impact
Tokens that don't actually perform the transfer and return false are still counted as a correct transfer and tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value.
Code Snippet
Tool used
Manual Review
Recommendation
Recommend using OpenZeppelin's SafeERC20 versions with the safeTransfer and safeTransferFrom functions that handle the return value check as well as non-standard-compliant tokens.
zimu - Unchecked return value of external tranfer call
zimu
high
Unchecked return value of external tranfer call
Summary
The return value of IERC20(token).transfer
is not checked. Actually, some tokens do not revert when failure and just return false state.
Vulnerability Detail
In DnGmxJuniorVaultManager._executeOperationToken
, IERC20(token).transfer(address(state.balancerVault), amountWithPremium)
is executed without check its return. When some tokens do not revert when failure and just return false, it won't act as the comments said "transfer token amount borrowed with premium back to balancer pool", and then, the pool would lost these assets.
In DnGmxBatchingManager.depositToken
, IERC20(token).transferFrom(msg.sender,address(this),amount) is the same.
Impact
Unchecked return value would possibly cause loss of assets.
Code Snippet
Tool used
Manual Review
Recommendation
Check the return value of transfer. If return false, then revert the execution.
zimu - Unchecked return value of external AAVE call of IPool interface
zimu
medium
Unchecked return value of external AAVE call of IPool interface
Summary
Unchecked return value of external AAVE call of IPool interface in some functions of DnGmxJuniorVaultManager.sol
. It is dangerous when a pool is working abnormal, i.e., liquidity drained, anomalous price fluctuation.
Vulnerability Detail
In function _executeRepay
and _executeWithdraw
of DnGmxJuniorVaultManager.sol
, state.pool.repay(token, amount, VARIABLE_INTEREST_MODE, address(this))
and state.pool.withdraw(token, amount, receiver)
are called without checking its return value.
According to the specification of aave\core-v3\contracts\interfaces\IPool.sol
, repay
and withdraw
return the final amount. When repay
and withdraw
return zero or an abnormal amount number without calling revert
, the fund would be lost.
Impact
Unchecked return value of external call to pool will suffer losses under abnormal circumstances.
Code Snippet
Tool used
Manual Review
Recommendation
Check return value, and implement some handles
simon135 - There is no input validation on `withdrawToken()` so an attacker can input any address as `from` and cause loss of funds
simon135
high
There is no input validation on withdrawToken()
so an attacker can input any address as from
and cause loss of funds
Summary
There is no input validation on withdrawToken()
so an attacker can input any address as from
and cause a loss of funds
Vulnerability Detail
an attacker can supply an address as from
parameter and cause loss of funds because its not msg.sender that is withdrawing and then the receiver
is the attacker
Impact
loss of funds
Code Snippet
dnGmxJuniorVault.withdraw(sGlpAmount, address(this), from);
amountOut = _convertToToken(token, receiver);
emit TokenWithdrawn(from, receiver, token, sGlpAmount, amountOut);
}
Tool used
Manual Review
Recommendation
only allow msg.sender to withdraw their funds or their approved
Duplicate of #79
clems4ever - The function redeem is unprotected
clems4ever
high
The function redeem is unprotected
Summary
In WithdrawPeriphery.sol redeemToken
is not protected, allowing stealing of user funds under conditions.
Vulnerability Detail
When a legitimate user desires to interact with WithdrawPeriphery, he has to first approve tokens to the contract. Unfortunately once he has approved his tokens to the contract an attacker can use redeemToken
to steal his funds because from
and receiver
are not checked.
Impact
Theft of user funds
Code Snippet
See the test labeled 2.unprotected_redeem
Tool used
Manual Review
Recommendation
Do not leave the from
parameter open. Use msg.sender
Duplicate of #79
0x0 - Inconsistent Storage Gaps
0x0
medium
Inconsistent Storage Gaps
Summary
Storage gaps are used with upgradable contracts to provide safety in new deployments. They allow for new storage variables to be added without overwriting existing state. There is an inconsistent number of gaps between the vaults.
Vulnerability Detail
There are 100 slots reserved in this implementation. In the junior vault there are 50, and there are 50 in the senior vault.
Impact
- This lack of consistency in the contract implementation could lead to confusion in upgrades and overwrites of state storage.
Code Snippet
uint256[100] private _gaps;
Tool used
Manual Review
Recommendation
- Have a consistent approach to assigning storage slots with upgradable contracts to avoid confusion and state collisions during deployments
clems4ever - Anyone can unpause deposit on DnGmxBatchingManager
clems4ever
medium
Anyone can unpause deposit on DnGmxBatchingManager
Summary
The issue is here: https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxBatchingManager.sol#L242
Anyone with no particular permission can unpause deposit on the contract.
Vulnerability Detail
The PoC is here https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/b7d6387e7e77e592bdd82668582ffefbd39ad43b
Impact
Users can start using the contract before it is ready to use (params are set by the owner) and this can mess up rewards and fees calculations.
Code Snippet
The PoC is here https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/b7d6387e7e77e592bdd82668582ffefbd39ad43b
Tool used
Test framework
Manual Review
Recommendation
Remove that line and perhaps add whenNotPaused modifier to the executeBatchDeposit
function.
Duplicate of #59
Bnke0x0 - Deflationary tokens are not supported
Bnke0x0
medium
Deflationary tokens are not supported
Summary
Vulnerability Detail
There are ERC20 tokens that may make certain customizations to their ERC20 contracts. One type of these tokens is deflationary tokens that charge a certain fee for every transfer() or transferFrom().
Impact
assume that the external ERC20 balance of the contract increases by the same amount as the amount parameter of the transferFrom.
Code Snippet
'IERC20(token).transferFrom(msg.sender, address(this), amount);'
'usdc.transferFrom(msg.sender, address(this), amount);'
'sGlp.transfer(address(dnGmxJuniorVault), glpToTransfer);'
'dnGmxJuniorVault.transfer(receiver, amount);'
'state.weth.transfer(state.feeRecipient, amount);'
'aUsdc.transfer(msg.sender, amount);'
'aUsdc.transferFrom(msg.sender, address(this), amount);'
Tool used
Manual Review
Recommendation
One possible mitigation is to measure the asset change right before and after the asset-transferring functions.
ctf_sec - Front-runnable DnGmxSeniorVault.sol#updateBorrowCap
ctf_sec
medium
Front-runnable DnGmxSeniorVault.sol#updateBorrowCap
Summary
The borrower can front run the updateBorrowCap and borrow more than intended.
Vulnerability Detail
The function updateBorrowCap is vulnerable to front-running.
function updateBorrowCap(address borrowerAddress, uint256 cap) external onlyOwner {
if (borrowerAddress != address(dnGmxJuniorVault) && borrowerAddress != address(leveragePool))
revert InvalidBorrowerAddress();
if (IBorrower(borrowerAddress).getUsdcBorrowed() >= cap) revert InvalidCapUpdate();
borrowCaps[borrowerAddress] = cap;
// give allowance to borrower to pull whenever required
aUsdc.approve(borrowerAddress, cap);
emit BorrowCapUpdated(borrowerAddress, cap);
}
the borrower can use borrow to front-run the updateBorrowCap.
function borrow(uint256 amount) external onlyBorrower {
// revert on invalid borrow amount
if (amount == 0 || amount > availableBorrow(msg.sender)) revert InvalidBorrowAmount();
// lazily harvest fees (harvest would return early if not enough rewards accrued)
dnGmxJuniorVault.harvestFees();
// transfers aUsdc to borrower
// but doesn't reduce totalAssets of vault since borrwed amounts are factored in
aUsdc.transfer(msg.sender, amount);
}
the onlyBorrower modifier restrict that only the dnGmxJuniorVault contract can call this function so far, how can we trigger the dnGmxJuniorVault to borrow from the senior vault?
The dnGmxJuniorVault call DnGmxSeniorVault.sol#borrow in DnGmxJuniorVaultManager.sol#rebalanceHedge
// rebalance the unhedged glp (increase/decrease basis the capped optimal token hedges)
_rebalanceUnhedgedGlp(state, optimalUncappedEthBorrow, optimalEthBorrow);
if (availableBorrow > 0) {
// borrow whatever is available since required > available
state.dnGmxSeniorVault.borrow(availableBorrow);
}
} else {
//No unhedged glp remaining so just pass same value in capped and uncapped (should convert back any ausdc back to sglp)
_rebalanceUnhedgedGlp(state, optimalEthBorrow, optimalEthBorrow);
// Take from LB Vault
state.dnGmxSeniorVault.borrow(targetDnGmxSeniorVaultAmount - currentDnGmxSeniorVaultAmount);
}
this function DnGmxJuniorVaultManager.sol#rebalanceHedge is called in the beforeWithdraw and afterDeposit hook in the junior Vault.
function beforeWithdraw(
uint256 assets,
uint256,
address
) internal override {
(uint256 currentBtc, uint256 currentEth) = state.getCurrentBorrows();
//rebalance of hedge based on assets after withdraw (before withdraw assets - withdrawn assets)
state.rebalanceHedge(currentBtc, currentEth, totalAssets() - assets, false);
}
function afterDeposit(
uint256,
uint256,
address
) internal override {
if (totalAssets() > state.depositCap) revert DepositCapExceeded();
(uint256 currentBtc, uint256 currentEth) = state.getCurrentBorrows();
//rebalance of hedge based on assets after deposit (after deposit assets)
state.rebalanceHedge(currentBtc, currentEth, totalAssets(), false);
}
Consider this case,
the admin owner wants to update the updateBorrowCap,
the old borrow cap is 100,
the admin wants to update the borrow to 50
A user detect this transaction.
He frontrun the updateBorrowCap, he call the deposit in Junior vault, which trigger the afterDeposit hook, which borrow from the senior vault.
and borrow 100 amount.
The transaction landed, the borrow cap is adjusted to 50 amount.
The user backrun the updateBorrowCap, he call the withdraw in Junior vault, which trigger the beforeWithdrawal hook, which borrow another 50 amount from the senior vualt.
Impact
User can borrow more than the admin intended.
Code Snippet
Tool used
Manual Review
Recommendation
Instead of setting the given amount, one can reduce from the current approval. By doing so, it checks whether the previous approval is spend.
0x0 - Two Storage Gap Implementations In Same Contract
0x0
medium
Two Storage Gap Implementations In Same Contract
Summary
Storage gaps provide state safety against collisions on upgrades. In one contract there are two implementations.
Vulnerability Detail
DnGmxBatchingManager
Line 46 implements a storage gap of 100 slots. Line 74 implements a further 50.
Impact
- Multiple implementations can create confusion during upgrades and lead to state being overwritten.
Code Snippet
uint256[100] private _gaps;
uint256[50] private __gaps2;
Tool used
Manual Review
Recommendation
- Be explicit with a single implementation to avoid confusion and collisions at a later upgrade time.
Prefix - feeRecipient can be set to unavailable address, leading to losing funds with withdrawFees
Prefix
low
feeRecipient can be set to unavailable address, leading to losing funds with withdrawFees
Medium- feeRecipient can be set to unavailable address, leading to losing funds with withdrawFees
Owner can set the feeRecipient to unavailable address, e.g. 0 by mistake. The next call to withdrawFees
loses all the accumulated fees from contract. Even if the owner quickly recognizes the mistake, malicious actor can frontrun next call to setFeeParams
with withdrawFees
.
Remediation
Consider setting the feeRecipient
in two steps like in following example: OpenZeppelin/openzeppelin-contracts#3620 .
rvierdiiev - Fees can be burnt by 3 party actor if state.feeRecipient is set to 0 in DnGmxJuniorVault
rvierdiiev
medium
Fees can be burnt by 3 party actor if state.feeRecipient is set to 0 in DnGmxJuniorVault
Summary
Fees can be burnt by 3 party actor if state.feeRecipient is set to 0.
Vulnerability Detail
DnGmxJuniorVault.withdrawFees
is a function that is allowed to call by anyone in order to send accumulated fees to the state.feeRecipient
param.
DnGmxJuniorVault.setFeeParams
function allows owner to provide state.feeRecipient
param. But it doesn't check if address is not 0. Also while initializing there is no state.feeRecipient
setting. That means that after the contract is created and initialized state.feeRecipient
param is address(0).
As the result when DnGmxJuniorVault.withdrawFees
function is called then all fees will be burnt to address(0). And what is more dangerous is that the function can be called by anyone. While protocol owners will notice the mistake and will try to change fee recipient someone can already burn their fees.
Similar problem has DnGmxJuniorVault.claimVestedGmx
, but in this case it's only callable by owner, so in case if owner will see that no fee recipient is provided he has time to do that.
Impact
Protocol fees can be burnt because of lack of input checking.
Code Snippet
function setFeeParams(uint16 _feeBps, address _feeRecipient) external onlyOwner {
if (state.feeRecipient != _feeRecipient) {
state.feeRecipient = _feeRecipient;
} else revert InvalidFeeRecipient();
if (_feeBps > 3000) revert InvalidFeeBps();
state.feeBps = _feeBps;
emit FeeParamsUpdated(_feeBps, _feeRecipient);
}
/// @notice withdraw accumulated WETH fees
function withdrawFees() external {
uint256 amount = state.protocolFee;
state.protocolFee = 0;
state.weth.transfer(state.feeRecipient, amount);
emit FeesWithdrawn(amount);
}
Tool used
Manual Review
Recommendation
Check that fee recipient is not address(0) in DnGmxJuniorVault.withdrawFees
and DnGmxJuniorVault.claimVestedGmx
functions.
cccz - Attacker can manipulate the pricePerShare to profit from future users' deposits
cccz
medium
Attacker can manipulate the pricePerShare to profit from future users' deposits
Summary
By manipulating and inflating the pricePerShare to a super high value, the attacker can cause all future depositors to lose a significant portion of their deposits to the attacker due to precision loss.
Vulnerability Detail
For example, in the DnGmxSeniorVault contract, a malicious early user can deposit() with 1 wei of USDC as the first depositor of the LToken, and get 1 wei of shares.
Then the attacker can send 10000e18 of aUSDC and inflate the price per share from 1.0000 to an extreme value of 1.0000e22 ( from (10000e18 ) / 1) .
As a result, the future user who deposits 9999e18 will only receive 0 (from 9999e18 * 1 / 10000e18) of shares token.
They will immediately lose all of their deposits.
Impact
The attacker can profit from future users' deposits. While the late users will lose part of their funds to the attacker.
Code Snippet
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L59-L71
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L192-L196
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/ERC4626/ERC4626Upgradeable.sol#L273-L277
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L370-L373
Tool used
Manual Review
Recommendation
Consider requiring a minimal amount of share tokens to be minted for the first minter, and send part of the initial mints as a permanent reserve to the DAO/treasury/deployer so that the pricePerShare can be more resistant to manipulation.
Duplicate of #37
Ruhum - Junior and Senior vault can't handle slippage
Ruhum
medium
Junior and Senior vault can't handle slippage
Summary
The ERC4626 contract doesn't have slippage checks built in. With time the ratio of assets to shares will increase. It won't be 1:1. Anybody who deposits or withdraws is at the risk of being sandwiched by MEV bots which will cause a loss of funds for the user.
Vulnerability Detail
The user-facing functions to deposit and withdraw assets in the DnGmxJuniorVault
and DnGmxSeniorVault
contracts don't offer any slippage protection. Anybody using these functions through the public mempool is at the risk of being sandwiched. The ERC4626 standard doesn't have slippage checks built-in. Developers have to add it themselves. When Tribe initially launched the standard, they provided a router contract with slippage checks, see here.
Impact
Any user depositing/withdrawing assets to/from the vault is at risk of being sandwiched. With the current scale of MEV, it's pretty likely that someone will pick up on this. Users will lose funds.
Code Snippet
Vaults use standard ERC4626 function that have no slippage checks:
- https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L388-L442
- https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L211-L270
Tool used
Manual Review
Recommendation
Allow the user to pass a minimum value to deposit()
and redeem()
as well as a maximum value to mint()
and withdraw()
. On the client side, the deposit/withdrawal should be previewed using the respective functions and that value should be passed to the tx.
clems4ever - The function withdraw is unprotected
clems4ever
high
The function withdraw is unprotected
Summary
In WithdrawPeriphery.sol withdrawToken
is not protected, allowing stealing of user funds under conditions.
Vulnerability Detail
When a legitimate user desires to interact with WithdrawPeriphery, he has to first approve tokens to the contract. Unfortunately once he has approved his tokens to the contract an attacker can use withdraw to steal his funds because from
and receiver
are not checked.
Impact
Theft of user funds
Code Snippet
See the test labeled 1.unprotected_withdraw
Tool used
Manual Review
Recommendation
Do not leave from open. Use msg.sender
Duplicate of #79
Bnke0x0 - ERC20 return values not checked
Bnke0x0
high
ERC20 return values not checked
Summary
Some tokens (like USDT) don't correctly implement the EIP20 standard and their transfer/transferFrom function return void instead of a successful boolean. Calling these functions with the correct EIP20 function signatures will always revert.
Vulnerability Detail
Impact
Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value. As there is a cToken with USDT as the underlying issue directly applies to the protocol.
Code Snippet
'IERC20(token).transferFrom(msg.sender, address(this), amount);'
'usdc.transferFrom(msg.sender, address(this), amount);'
'aUsdc.transferFrom(msg.sender, address(this), amount);'
Tool used
Manual Review
Recommendation
We recommend using OpenZeppelin’s SafeERC20 versions with the safeTransfer and safeTransferFrom functions that handle the return value check as well as non-standard-compliant tokens.
Nyx - Attacker can steal funds
Nyx
high
Attacker can steal funds
Summary
Attacker can steal funds with withdrawToken() function.
Vulnerability Detail
Missing validation on withdrawTokens from parameter can cause attackers to steal funds.
Impact
Users who deposited dnGmxJuniorVault can lose their funds.
Code Snippet
it('withdrawToken - attack', async () => {
const { usdc, users, periphery, gmxVault, dnGmxJuniorVault, dnGmxSeniorVault } = await dnGmxJuniorVaultFixture();
await dnGmxSeniorVault.connect(users[1]).deposit(parseUnits('100', 6), users[1].address);
const amount = parseEther('100');
const withdrawAmount = parseEther('20');
await dnGmxJuniorVault.connect(users[0]).deposit(amount, users[0].address);
await dnGmxJuniorVault.connect(users[0]).approve(periphery.address, constants.MaxUint256);
console.log('user[0] balance', await dnGmxJuniorVault.balanceOf(users[0].address));
const usdcBalBefore = await usdc.balanceOf(users[2].address);
console.log('usdcBalBefore', usdcBalBefore.toString());
const tx = periphery
.connect(users[2])
.withdrawToken(users[0].address, usdc.address, users[2].address, withdrawAmount);
await expect(tx).to.emit(periphery, 'TokenWithdrawn');
const usdcBalAfter = await usdc.balanceOf(users[2].address); // attacker balance
console.log('usdcBalAfter', usdcBalAfter.toString());
console.log('user[0] balance', await dnGmxJuniorVault.balanceOf(users[0].address)); // user balance
Tool used
Manual Review
Recommendation
from needs to be msg.sender.
Duplicate of #79
simon135 - if the owner is resetting the fee recipient an attacker can frontrun the the resetting and the new fee recipient will not get the fees
simon135
medium
if the owner is resetting the fee recipient an attacker can frontrun the the resetting and the new fee recipient will not get the fees
Summary
if the owner is resetting the fee recipient an attacker can front-run the resetting and the new fee recipient will not get the fees
Vulnerability Detail
if the owner is resetting the fee recipient an attacker can front-run the resetting and the new fee recipient will not get the fees
because there is no access control on the withdrawFees()
function anyone can call it maybe if the fee recipient has a deal with the actor who initiated withdrawFees()
.The problem also is that the owner changes the recipient but why is it fair they miss out on the fees on tx before
Impact
the new recipient doesn't get fees
Code Snippet
function withdrawFees() external {
uint256 amount = state.protocolFee;
state.protocolFee = 0;
state.weth.transfer(state.feeRecipient, amount);
emit FeesWithdrawn(amount);
}
Tool used
Manual Review
Recommendation
make it only admin or some keeper can call it
function withdrawFees() external onlyAdmin{
}
clems4ever - Anyone can trigger a transfer of protocol fees
clems4ever
high
Anyone can trigger a transfer of protocol fees
Summary
Transfer of protocol fee is not protected. The attacker can trigger the transfer whatever the parameters are set to. It could be problematic if the protocol owner made a mistake with the fee recipient address.
Vulnerability Detail
Anyone can call withdrawFees() method on DnGmxJuniorVault leading to unwanted transfer of funds to whoever was the wrong recipient.
The issue is here, the method should be protected: https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L306
Impact
This can lead to loss of funds or transfer to an unexpected address if the protocol owner did not select the right fee recipient.
Code Snippet
Tool used
Manual Review & Test Framework
Recommendation
Add a require testing that msg.sender is actually the fee recipient. This would limit the damages because it would require that the owner of the wrong address be aware that some funds are waiting for him. In the meantime, the protocol owner could still update the fee recipient to target the right account instead.
0x52 - DnGmxJuniorVaultManager#_totalAssets current implementation doesn't properly maximize or minimize
0x52
medium
DnGmxJuniorVaultManager#_totalAssets current implementation doesn't properly maximize or minimize
Summary
The maximize input to DnGmxJuniorVaultManager#_totalAssets indicates whether to either maximize or minimize the NAV. Internal logic of the function doesn't accurately reflect that because under some circumstances, maximize = true actually returns a lower value than maximize = false.
Vulnerability Detail
uint256 unhedgedGlp = (state.unhedgedGlpInUsdc + dnUsdcDepositedPos).mulDivDown(
PRICE_PRECISION,
_getGlpPrice(state, !maximize)
);
// calculate current borrow amounts
(uint256 currentBtc, uint256 currentEth) = _getCurrentBorrows(state);
uint256 totalCurrentBorrowValue = _getBorrowValue(state, currentBtc, currentEth);
// add negative part to current borrow value which will be subtracted at the end
// convert usdc amount into glp amount
uint256 borrowValueGlp = (totalCurrentBorrowValue + dnUsdcDepositedNeg).mulDivDown(
PRICE_PRECISION,
_getGlpPrice(state, !maximize)
);
// if we need to minimize then add additional slippage
if (!maximize) unhedgedGlp = unhedgedGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
if (!maximize) borrowValueGlp = borrowValueGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
To maximize the estimate for the NAV of the vault underlying debt should minimized and value of held assets should be maximized. Under the current settings there is a mix of both of those and the function doesn't consistently minimize or maximize. Consider when NAV is "maxmized". Under this scenario the value of when estimated the GlpPrice is minimized. This minimizes the value of both the borrowedGlp (debt) and of the unhedgedGlp (assets). The result is that the NAV is not maximized because the value of the assets are also minimized. In this scenario the GlpPrice should be maximized when calculating the assets and minimized when calculating the debt. The reverse should be true when minimizing the NAV. Slippage requirements are also applied incorrectly when adjusting borrowValueGlp. The current implementation implies that if the debt were to be paid back that the vault would repay their debt for less than expected. When paying back debt the slippage should imply paying more than expected rather than less, therefore the slippage should be added rather than subtracted.
Impact
DnGmxJuniorVaultManager#_totalAssets doesn't accurately reflect NAV. Since this is used when determining critical parameters it may lead to inaccuracies.
Code Snippet
Tool used
Manual Review
Recommendation
To properly maximize the it should assume the best possible rate for exchanging it's assets. Likewise to minimize it should assume it's debt is a large as possible and this it encounters maximum possible slippage when repaying it's debt. I recommend the following changes:
uint256 unhedgedGlp = (state.unhedgedGlpInUsdc + dnUsdcDepositedPos).mulDivDown(
PRICE_PRECISION,
- _getGlpPrice(state, !maximize)
+ _getGlpPrice(state, maximize)
);
// calculate current borrow amounts
(uint256 currentBtc, uint256 currentEth) = _getCurrentBorrows(state);
uint256 totalCurrentBorrowValue = _getBorrowValue(state, currentBtc, currentEth);
// add negative part to current borrow value which will be subtracted at the end
// convert usdc amount into glp amount
uint256 borrowValueGlp = (totalCurrentBorrowValue + dnUsdcDepositedNeg).mulDivDown(
PRICE_PRECISION,
_getGlpPrice(state, !maximize)
);
// if we need to minimize then add additional slippage
if (!maximize) unhedgedGlp = unhedgedGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
- if (!maximize) borrowValueGlp = borrowValueGlp.mulDivDown(MAX_BPS - state.slippageThresholdGmxBps, MAX_BPS);
+ if (!maximize) borrowValueGlp = borrowValueGlp.mulDivDown(MAX_BPS + state.slippageThresholdGmxBps, MAX_BPS);
yixxas - `executeBatchDeposit()` is missing access control
yixxas
medium
executeBatchDeposit()
is missing access control
Summary
executeBatchDeposit()
is callable by anyone which _unpause()
deposits. 15 minutes cool down set by the protocol can be bypassed due to this.
Vulnerability Detail
Calling executeBatchDeposit()
will unpause()
deposit as it is required for batch deposit, but this can be called at anytime by anyone.
Impact
Pause state that is being used to prevent deposits can be bypassed.
Code Snippet
Tool used
Manual Review
Recommendation
Add onlyKeeper
modifier to this function.
Duplicate of #59
0xmuxyz - Anyone (any external users) can mint `any amount` of `vault shares` - because of lack of access control modifier on `mint()` function
0xmuxyz
high
Anyone (any external users) can mint any amount
of vault shares
- because of lack of access control modifier on mint()
function
Summary
- Anyone (any external users) can mint
any amount
ofvault shares
- because of lack of access control modifier onmint()
function
Vulnerability Detail
- Lack of
access control modifier
onmint()
function that allow any external users to be able to mintany amount
ofvault shares
.
Impact
- There is no access control modifier on
mint()
function in the DnGmxJuniorVault.sol and DnGmxSeniorVault.sol.- As a result, any external users can mint
any amount
ofvault shares
- This vulnerability lead to an exploit that give large vault shares to malicious attackers without proper efforts.
- As a result, any external users can mint
Code Snippet
-
There is no access control modifier on
mint()
function in the DnGmxJuniorVault.sol
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxJuniorVault.sol#L403-L412 -
There is no access control modifier on
mint()
function in the DnGmxSeniorVault.sol.
https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/vaults/DnGmxSeniorVault.sol#L228-L238
Tool used
- Manual Review
Recommendation
- It should add an access control modifier to
mint()
function in order to thatonly privileged-users
can access this function by using access control libraries in @openzeppelin/contracts (such as onlyOwner, onlyRole, etc) for example:- onlyOwner by Ownable.sol:https://docs.openzeppelin.com/contracts/2.x/access-control#ownership-and-ownable
- onlyRole by AccessControl.sol:https://docs.openzeppelin.com/contracts/4.x/access-control#role-based-access-control
defsec - Use `safeTransfer/safeTransferFrom` consistently instead of `transfer/transferFrom`
defsec
medium
Use safeTransfer/safeTransferFrom
consistently instead of transfer/transferFrom
Summary
Replace transferFrom()
with safeTransferFrom()
since _tokenIn
can be any ERC20
token implementation. If transferFrom()
does not return a value (e.g., USDT), the transaction reverts because of a decoding error.
Vulnerability Detail
Replace transferFrom()
with safeTransferFrom()
since _tokenIn
can be any ERC20
token implementation. If transferFrom()
does not return a value (e.g., USDT), the transaction reverts because of a decoding error. Revert without error.
Impact
It is good to add a require() statement that checks the return value of token transfers or to use something like OpenZeppelin’s safeTransfer/safeTransferFrom unless one is sure the given token reverts in case of a failure. Failure to do so will cause silent failures of transfers and affect token accounting in the contract.
Code Snippet
Tool used
Manual Review
Recommendation
Consider using safeTransfer/safeTransferFrom or require() consistently.
0x52 - WithdrawPeriphery uses incorrect value for MAX_BPS which will allow much higher slippage than intended
0x52
medium
WithdrawPeriphery uses incorrect value for MAX_BPS which will allow much higher slippage than intended
Summary
WithdrawPeriphery accidentally uses an incorrect value for MAX_BPS which will allow for much higher slippage than intended.
Vulnerability Detail
uint256 internal constant MAX_BPS = 1000;
BPS is typically 10,000 and using 1000 is inconsistent with the rest of the ecosystem contracts and tests. The result is that slippage values will be 10x higher than intended.
Impact
Unexpected slippage resulting in loss of user funds, likely due to MEV
Code Snippet
Tool used
Manual Review
Recommendation
Correct MAX_BPS:
- uint256 internal constant MAX_BPS = 1000;
+ uint256 internal constant MAX_BPS = 10_000;
clems4ever - Share manipulation in senior vault
clems4ever
high
Share manipulation in senior vault
Summary
First users can manipulate share allocation to ensure next users receive less shares than due.
Vulnerability Detail
By depositing into the senior vault a user can provide capital and gets shares of the total locked capital. The calculation of shares is pretty straightforward:
shares=(totalSupply/totalAssets)
where
function totalAssets() public view override(IERC4626, ERC4626Upgradeable)
returns (uint256 amount) {
amount = aUsdc.balanceOf(address(this));
amount += totalUsdcBorrowed();
}
and totalSupply is the number of shares issued yet.
so a malicious user can deposit some capital first, and then send aUsdc to the contract, which would modify the ratio used to convert deposit to shares.
When the ratio is very small, rounding errors become significant. If we take the example where the number of shares is 19
, and the total deposits amount to 10000
USDC, a new user depositing 1000
USDC will receive only 1
share, almost 50%
less than what he's due.
If enough deposits accumulate after this manipulation, the attacker shares are worth more than what he deposited (because next shares are truncated compared to deposits).
Impact
Malicious users can manipulate share prices and withdraw other users funds.
Code Snippet
See the test labeled 3.share_manipulation
https://github.com/sherlock-audit/2022-10-rage-trade-clems4ever/commit/548b071b47bb05916f24c0f2459ae1cde9dd16a0
Tool used
Manual Review
Recommendation
Duplicate of #37
0x52 - Early depositors to DnGmxSeniorVault can manipulate exchange rates to steal funds from later depositors
0x52
high
Early depositors to DnGmxSeniorVault can manipulate exchange rates to steal funds from later depositors
Summary
To calculate the exchange rate for shares in DnGmxSeniorVault it divides the total supply of shares by the totalAssets of the vault. The first deposit can mint a very small number of shares then donate aUSDC to the vault to grossly manipulate the share price. When later depositor deposit into the vault they will lose value due to precision loss and the adversary will profit.
Vulnerability Detail
function convertToShares(uint256 assets) public view virtual returns (uint256) {
uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets());
}
Share exchange rate is calculated using the total supply of shares and the totalAsset. This can lead to exchange rate manipulation. As an example, an adversary can mint a single share, then donate 1e8 aUSDC. Minting the first share established a 1:1 ratio but then donating 1e8 changed the ratio to 1:1e8. Now any deposit lower than 1e8 (100 aUSDC) will suffer from precision loss and the attackers share will benefit from it.
This same vector is present in DnGmxJuniorVault.
Impact
Adversary can effectively steal funds from later users
Code Snippet
Tool used
Manual Review
Recommendation
Initialize should include a small deposit, such as 1e6 aUSDC that mints the share to a dead address to permanently lock the exchange rate:
aUsdc.approve(address(pool), type(uint256).max);
IERC20(asset).approve(address(pool), type(uint256).max);
+ deposit(1e6, DEAD_ADDRESS);
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.