Platform: Code4rena
Start Date: 21/08/2023
Pot Size: $125,000 USDC
Total HM: 26
Participants: 189
Period: 16 days
Judge: GalloDaSballo
Total Solo HM: 3
Id: 278
League: ETH
Rank: 102/189
Findings: 3
Award: $73.26
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xWagmi
Also found by: 836541, Bauchibred, GangsOfBrahmin, Hama, IceBear, Inspecktor, Matin, MohammedRizwan, catellatech, erebus, lsaudit, niki, okolicodes, ravikiranweb3, tapir, vangrim, zaevlad
46.2486 USDC - $46.25
The PerpetualAtlanticVaultLP.sol
contract is based on the ERC4626 where the shares are calculated based on the deposit value. Contest readme.md specified this contracts as Contract for the Perpetual Atlantic Vault LP (ERC4626)
.
By depositing large amount as initial deposit, initial depositor can influence the future depositors value, deposit()
is given as below,
File: contracts/perp-vault/PerpetualAtlanticVaultLP.sol 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"); perpetualAtlanticVault.updateFunding(); // Need to transfer before minting or ERC777s could reenter. collateral.transferFrom(msg.sender, address(this), assets); _mint(receiver, shares); _totalCollateral += assets; emit Deposit(msg.sender, receiver, assets, shares); }
As seen in function, Shares are minted based on the deposit value. By depositing large amount as initial deposit, first depositor can take advantage over other depositors. Future depositors are forced for huge value of asset to deposit. It is not practically possible for all the users and this could directly affect on the attrition of users towards this system.
As seen in deposit()
, it has
File: contracts/perp-vault/PerpetualAtlanticVaultLP.sol function previewDeposit(uint256 assets) public view returns (uint256) { return convertToShares(assets); } /// @notice Returns the amount of shares recieved for a given amount of assets function convertToShares( uint256 assets ) public view returns (uint256 shares) { uint256 supply = totalSupply; uint256 rdpxPriceInAlphaToken = perpetualAtlanticVault.getUnderlyingPrice(); uint256 totalVaultCollateral = totalCollateral() + ((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8); return supply == 0 ? assets : assets.mulDivDown(supply, totalVaultCollateral); }
The function convertToShares() is responsible to return the corresponding share of a user with respect to his deposited assets.
One of the main problems with these tokens lies in the fact that the first depositor can manipulate the unit share price in a bad way. The ratio of totalSupply/totalAssets determines the corresponding share of a deposited asset. As it is clear from the ratio if the first user deposits 1 Wei, will get 1 Wei worth of share.
Now For example: consider a below scenario:
Consider, Given a vault with DAI as the underlying asset:
The shares are calculated as following per the contract which can be checked here
return supply == 0 ? assets : assets.mulDivDown(supply, totalVaultCollateral); }
Manual review
Consider requiring a minimal amount of share tokens to be minted for the first minter to prevent such manipulations.
Note: It is to be noted here that openzeppelin has fixed ERC4626 inflation attack issue in their latest version. Reference here. The ERC4626 seems to be referred from solmate ERC4626 implementation and it looks very much same which can be checked here, However, consider using openzeppelin ERC4626 for vault in contract and can be checked here
ERC4626
#0 - c4-pre-sort
2023-09-07T13:29:39Z
bytes032 marked the issue as duplicate of #863
#1 - c4-pre-sort
2023-09-11T09:10:11Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-18T12:49:34Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: Vagner
Also found by: 0Kage, 0xCiphky, 0xnev, ABAIKUNANBAEV, Aymen0909, Evo, KmanOfficial, MohammedRizwan, T1MOH, Viktor_Cortess, Yanchuan, ak1, alexzoid, bin2chen, codegpt, hals, ladboy233, mrudenko, nemveer, oakcobalt, peakbolt, pep7siup, qbs, said, savi0ur, tapir, wintermute, zaevlad, zzebra83
7.8372 USDC - $7.84
In UniV2LiquidityAmo.sol
, _sendTokensToRdpxV2Core()
function is used to send back the tokens to rdpxV2Core
contract address.
File: contracts/amo/UniV2LiquidityAmo.sol function _sendTokensToRdpxV2Core() internal { uint256 tokenABalance = IERC20WithBurn(addresses.tokenA).balanceOf( address(this) ); uint256 tokenBBalance = IERC20WithBurn(addresses.tokenB).balanceOf( address(this) ); // transfer token A and B from this contract to the rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, tokenABalance ); IERC20WithBurn(addresses.tokenB).safeTransfer( addresses.rdpxV2Core, tokenBBalance ); emit LogAssetsTransfered(msg.sender, tokenABalance, tokenBBalance); }
The issue here the function does not Syncs asset reserves with contract balances that is sent back to rdpxV2Core
contract address.
Here, tokenA and tokenB are the rdpx
and weth
addresses respectively.
struct Addresses { // token A address address tokenA; // rdpx // token B address address tokenB; // weth
As they are sent back to rdpxV2Core
contract address, they needs to sync the token balances as these are the part of reserveAsset array
/* Inital tokens in the reserve index0: ZERO address index1: weth index2: rdpx index3: dpxEth index4: crv */ /// @notice Array containg the reserve assets ReserveAsset[] public reserveAsset;
This is actually synced by this function in RdpxV2Core.sol
It is to be noted that, This is only the case with UniV2LiquidityAmo.sol
, However if you see UniV3LiquidityAmo.sol
_sendTokensToRdpxV2Core()
function has sync the token balances which can be seen as below,
File: contracts/amo/UniV3LiquidityAmo.sol function _sendTokensToRdpxV2Core(address tokenA, address tokenB) internal { uint256 tokenABalance = IERC20WithBurn(tokenA).balanceOf(address(this)); uint256 tokenBBalance = IERC20WithBurn(tokenB).balanceOf(address(this)); // transfer token A and B from this contract to the rdpxV2Core IERC20WithBurn(tokenA).safeTransfer(rdpxV2Core, tokenABalance); IERC20WithBurn(tokenB).safeTransfer(rdpxV2Core, tokenBBalance); // sync token balances >> IRdpxV2Core(rdpxV2Core).sync(); emit LogAssetsTransfered(tokenABalance, tokenBBalance, tokenA, tokenB); }
To avoid the unexpected accounting behavior, token balances should be sync.
Manual review
function _sendTokensToRdpxV2Core() internal { uint256 tokenABalance = IERC20WithBurn(addresses.tokenA).balanceOf( address(this) ); uint256 tokenBBalance = IERC20WithBurn(addresses.tokenB).balanceOf( address(this) ); // transfer token A and B from this contract to the rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, tokenABalance ); IERC20WithBurn(addresses.tokenB).safeTransfer( addresses.rdpxV2Core, tokenBBalance ); + // sync token balances + IRdpxV2Core(rdpxV2Core).sync(); emit LogAssetsTransfered(msg.sender, tokenABalance, tokenBBalance); }
Other
#0 - c4-pre-sort
2023-09-09T03:45:58Z
bytes032 marked the issue as duplicate of #798
#1 - c4-pre-sort
2023-09-09T04:09:31Z
bytes032 marked the issue as duplicate of #269
#2 - c4-pre-sort
2023-09-11T11:58:41Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-15T18:11:41Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: juancito
Also found by: 0xDING99YA, 0xTiwa, 0xkazim, 0xnev, ABA, ArmedGoose, Aymen0909, Bauchibred, Evo, IceBear, KrisApostolov, MohammedRizwan, Nikki, QiuhaoLi, T1MOH, Toshii, WoolCentaur, Yanchuan, __141345__, asui, bart1e, carrotsmuggler, catellatech, chaduke, codegpt, deadrxsezzz, degensec, dethera, dirk_y, erebus, ether_sky, gjaldon, glcanvas, jasonxiale, josephdara, klau5, kodyvim, ladboy233, lsaudit, minhquanym, parsely, peakbolt, pep7siup, rvierdiiev, said, savi0ur, sces60107, tapir, ubermensch, volodya, zzebra83
19.1724 USDC - $19.17
Number | Issue | Instances | |
---|---|---|---|
[L‑01] | openzeppelin's setupRole() is deprecated | 7 | |
[L‑02] | set max limit of slippageTolerance in setSlippageTolerance() | 2 | |
[L‑03] | Calls inside loops that may address DoS | 1 | |
[L‑04] | Admin may not receive usdc/usdt if blacklisted in case of emergency | 1 | |
[L‑05] | Insufficient validations in setAddresses() can cause both tokenA and tokenB same | 2 | |
[L‑06] | emergencyWithdraw() functionality should be added in UniV3LiquidityAmo.sol like other contracts | 1 | |
[L‑07] | TickMath.sol might revert in solidity version 0.8.19 as used in contracts | 1 | |
[L‑08] | Update solc version and use unchecked in Uniswap related libraries like LiquidityAmounts.sol | 1 | |
[L‑09] | Solmate safeTransferLib.sol functions does not check the codesize of the token address, which may lead to fund loss | 1 | |
[L‑10] | An issue with TransferHelper.sol which is extensively used in UniV3LiquidityAmo.sol | 1 | |
[L‑11] | Do not hard code the contract address, pass as constructor argument in UniV3LiquidityAmo.sol | 1 | |
[L‑12] | In RdpxDecayingBonds.sol , mint() does not verify that the bond expiry has passed/expired | 1 | |
[L‑13] | In DpxEthToken.sol , mint() , burn() and burnFrom() should only be called when the contract is not paused | 3 | |
[L‑14] | addToContractWhitelist() does not check the contract code existence before whitelisting | 1 | |
[L‑15] | In RdpxV2Bond.sol , mint() should be called when the contract is not paused | 1 | |
[L‑16] | Misleading Natspec on getLpPrice() in UniV2LiquidityAmo.sol | 1 | |
[L‑17] | deadline should be passed as an argument instead of hardcoding it | contracts |
setupRole()
is deprecatedContracts has used _setupRole()
in functions for setting the roles. However _setupRole()
is deprecated by openzeppelin in favor of _grantRole()
. This can be checked and confirmed at openzeppelin releases here
There are below instances of this issue:
Instance 1 in UniV2LiquidityAmo.sol
at L-58
Instance 2 in UniV3LiquidityAmo.sol
at L-80
Instance 3 in RdpxV2Bond.sol
at L-25,L-26
Instance 4 in RdpxV2Core.sol
at L-125
Instance 5 in RdpxDecayingBonds.sol
at L-61,L-62
Instance 6 in PerpetualAtlanticVault.sol
at L-126
Instance 7 in ReLPContract.sol
at L-80
Use _grantRole()
instead of _setupRole()
slippageTolerance
in setSlippageTolerance()
setSlippageTolerance()
is used to set the slippageTolerance
which is used in swaps 1e8 precision. The contract has bydefault a slippageTolerance = 5e5
which is equivalent to 0.5%
and this function is used to set same in future. The issue here is it checks the min slippageTolerance
must be greater than zero
but it does not check the maximum slippageTolerance
which can be set to any value by admin.
There is instance of this issue:
Another instance of this issue:
Add check of max slippageTolerance
so that it can be set within limits by admin. This must be ensured for users safety in case of malicious admin or admin may set the slippageTolerance
to any value.
In UniV2LiquidityAmo.sol
, emergencyWithdraw()
function,
Calls to external contracts inside a loop are dangerous because it could lead to DoS if one of the calls reverts or execution runs out of gas. Such issue also introduces chance of problems with the gas limits.
Per SWC-113: External calls can fail accidentally or deliberately, which can cause a DoS condition in the contract. To minimize the damage caused by such failures, it is better to isolate each external call into its own transaction that can be initiated by the recipient of the call.
Reference link- https://swcregistry.io/docs/SWC-113
Below are few instance of this issue:
Instance 1 in UniV2LiquidityAmo.sol
at L-149
File: contracts/amo/UniV2LiquidityAmo.sol function emergencyWithdraw( address[] calldata tokens ) external onlyRole(DEFAULT_ADMIN_ROLE) { IERC20WithBurn token; for (uint256 i = 0; i < tokens.length; i++) { token = IERC20WithBurn(tokens[i]); >> token.safeTransfer(msg.sender, token.balanceOf(address(this))); } emit LogEmergencyWithdraw(msg.sender, tokens);
In UniV2LiquidityAmo.sol
, emergencyWithdraw()
is used to transfer the tokens in contract to msg.sender i.e admin address. However it is to be noted here that tokens can also be usdc or usdt whihc has user black listing feature. If the admin address is blacklisted then as the usdc or usdt can not be transferred to admin address. Therefore in case of emergency for which the function has been designed for wont work for usdc or usdt tokens.
There is 1 instance of this issue:
File: contracts/amo/UniV2LiquidityAmo.sol function emergencyWithdraw( address[] calldata tokens ) external onlyRole(DEFAULT_ADMIN_ROLE) { IERC20WithBurn token; for (uint256 i = 0; i < tokens.length; i++) { token = IERC20WithBurn(tokens[i]); token.safeTransfer(msg.sender, token.balanceOf(address(this))); } emit LogEmergencyWithdraw(msg.sender, tokens); }
Add function to transfer the usdc,usdt or function which can blacklist the users from receiving tokens and put the admin access control to it.
For example:
function emergencyWithdraw(address token, address recipient) external onlyRole(DEFAULT_ADMIN_ROLE) { IERC20WithBurn token; token.safeTransfer(recipient, token.balanceOf(address(this))); } emit LogEmergencyWithdraw(recipient, token); }
setAddresses()
can cause both tokenA
and tokenB
sameIn UniV2LiquidityAmo.sol
, setAddresses()
is used to set different addresses which is shown below,
File: contracts/amo/UniV2LiquidityAmo.sol function setAddresses( address _tokenA, address _tokenB, address _pair, address _rdpxV2Core, address _rdpxOracle, address _ammFactory, address _ammRouter ) external onlyRole(DEFAULT_ADMIN_ROLE) { require( _tokenA != address(0) && _tokenB != address(0) && _pair != address(0) && _rdpxV2Core != address(0) && _rdpxOracle != address(0) && _ammFactory != address(0) && _ammRouter != address(0), "reLPContract: address cannot be 0" ); addresses = Addresses({ tokenA: _tokenA, tokenB: _tokenB, pair: _pair, rdpxV2Core: _rdpxV2Core, rdpxOracle: _rdpxOracle, ammFactory: _ammFactory, ammRouter: _ammRouter }); }
The issue here is, it is possible the tokenA and tokenB can be same however this should not be the desired behavior as tokenA will be set to rdpx
and tokenB to weth
but there is no validation that checks both are not same.
There instance of this issue:
Another instance of this issue:
File: contracts/amo/UniV2LiquidityAmo.sol function setAddresses( address _tokenA, address _tokenB, address _pair, address _rdpxV2Core, address _rdpxOracle, address _ammFactory, address _ammRouter ) external onlyRole(DEFAULT_ADMIN_ROLE) { require( _tokenA != address(0) && _tokenB != address(0) && _pair != address(0) && _rdpxV2Core != address(0) && _rdpxOracle != address(0) && _ammFactory != address(0) && _ammRouter != address(0), "reLPContract: address cannot be 0" ); + require(_tokenA != _tokenB, "tokens can not be same"); addresses = Addresses({ tokenA: _tokenA, tokenB: _tokenB, pair: _pair, rdpxV2Core: _rdpxV2Core, rdpxOracle: _rdpxOracle, ammFactory: _ammFactory, ammRouter: _ammRouter }); }
emergencyWithdraw()
functionality should be added in UniV3LiquidityAmo.sol
like other contractsemergencyWithdraw()
function is used in case of emergency, However this is missing in UniV3LiquidityAmo.sol
contract. Other contracts like RdpxDecayingBonds.sol
, PerpetualAtlanticVault.sol
, RdpxV2Core.soland
UniV2LiquidityAmo.solhave
emergencyWithdraw()`. This should be added in contract to use incase of emergency.
There is 1 instance of this issue:
Add emergencyWithdraw()
function in contract. Refer the above listed contracts for similar implementation.
TickMath.sol
might revert in solidity version 0.8.19 as used in contractsUniswapV3’s TickMath library was changed to allow compilations for solidity version 0.8. However, adjustments to account for the implicit overflow behavior that the contract relies upon were not performed.
This has been extensively used in most of contracts. This also includes contracts which are in scope as well as out of scope contracts.
Lets's take an example:
UniV3LiquidityAmo.sol
is compiled with version 0.8.19 and has used this library in contract which can be seen as below,
import "../uniswap_V3/libraries/TickMath.sol";
In the worst case, it could be that the library always reverts (instead of overflowing as in previous versions), leading to a broken contracts(contract which have used it).
Upon finding the version of TickMath.sol
, it has used pragma solidity >=0.5.0
which can be seen as below,
pragma solidity >=0.5.0;
The same pragma solidity >=0.5.0; instead of pragma solidity >=0.5.0 <0.8.0; adjustments needs to made for the implicit overflow behavior in used contracts.
Follow the implementation of the official TickMath 0.8 branch which uses unchecked blocks for every function.
LiquidityAmounts.sol
The LiquidityAmounts.sol
has been used in most of contracts including in UniV3LiquidityAmo.sol
and it is referenced from Uniswap codebase which is intended to work with Solidity compiler <0.8. These older versions have unchecked arithmetic by default and the code takes it into account.
However, dopex contracs code is intended to work with Solidity compiler 0.8.19 which doesn't have unchecked arithmetic by default.
For example, UniV3LiquidityAmo.sol
can be checked here
Hence, to port the code, it has to be turned on via unchecked keyword.
For example, type(uint).max reverts for v0.8 and returns type(uint).max for older version
LiquidityAmounts.sol
in unchecked.In PerpetualAtlanticVaultLP.sol contract, The contract has used Solmate safeTransferLib.sol which doesn't check the existence of code at the token address. This is a known issue while using solmate's libraries.
Per the Solmate safeTransferLib.sol,
Note that none of the functions in this library check that a token has code at all! .....
Hence using safeTransferLib.sol library may lead to miscalculation of funds and may lead to loss of funds , because if safetransfer() and safetransferfrom() are called on a token address that doesn't have contract in it, it will always return success, bypassing the return value check. Due to this protocol will think that funds has been transferred and successful , and records will be accordingly calculated, but in reality funds were never transferred.
Use openzeppelin's safeERC20 which takes care of token code existence.
TransferHelper.sol
which is extensively used in UniV3LiquidityAmo.sol
UniV3LiquidityAmo.sol
has used the TransferHelper.sol
which is basically copied from uniswap. This TransferHelper library has been used for safeApprove()
, safeTransfer()
in UniV3LiquidityAmo.sol
contract. The issue is the library functions does not check the token address existence and it also does not check the address(0) validations for token address. There was an exploit happened due to not checking the address(0) because of the use of this library which can be referenced here,
According to Certik’s analysis:
One of the root causes of the vulnerability was the fact that tokenAddress.safeTransferFrom() does not revert when the tokenAddress is the zero (null) address (0x0…000)
Check token address is not address(0) and check the existence of token address in UniV3LiquidityAmo.sol OR it should be fixed in TransferHelper.sol
which will reflect the changes wherever it is being used.
UniV3LiquidityAmo.sol
In UniV3LiquidityAmo.sol
, univ3_factory
, univ3_positions
, univ3_router
has been hardcoded. This should not be hardcoded in constructor though the contracts will only be deployed on Arbitrum for now but in case it deploys on other chains, such implementation is not recommended. These addresses should be passed as an constructor arguments.
There are 3 instances of this issue:
constructor(address _rdpx, address _rdpxV2Core) { rdpx = _rdpx; rdpxV2Core = _rdpxV2Core; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); univ3_factory = IUniswapV3Factory( 0x1F98431c8aD98523631AE4a59f267346ea31F984 ); univ3_positions = INonfungiblePositionManager( 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 ); univ3_router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); }
+ // univ3_factory = 0x1F98431c8aD98523631AE4a59f267346ea31F984 + // univ3_positions = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 + // univ3_router = 0xE592427A0AEce92De3Edee1F18E0157C05861564 - constructor(address _rdpx, address _rdpxV2Core) { + constructor(address _rdpx, address _rdpxV2Core, _univ3_factory, _univ3_positions, _univ3_router) { rdpx = _rdpx; rdpxV2Core = _rdpxV2Core; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); - univ3_factory = IUniswapV3Factory( - 0x1F98431c8aD98523631AE4a59f267346ea31F984 - ); + univ3_factory = IUniswapV3Factory(_univ3_factory); - univ3_positions = INonfungiblePositionManager( - 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 - ); + univ3_positions = INonfungiblePositionManager(_univ3_positions); - univ3_router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + univ3_router = ISwapRouter(_univ3_router); }
RdpxDecayingBonds.sol
, mint()
does not verify that the bond expiry
has passed/expiredIn RdpxDecayingBonds.sol
contract, expiry
is used in mint()
function for the expiry timestamp of the bond. The issue here mint()
is not validating if the expiry
is still running or not, Without expiry
verification the functions keeps allowing to be made after the stipulated time or expiry or it may end immediately without letting any one know.
File: contracts/decaying-bonds/RdpxDecayingBonds.sol function mint( address to, uint256 expiry, uint256 rdpxAmount ) external onlyRole(MINTER_ROLE) { _whenNotPaused(); require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter"); uint256 bondId = _mintToken(to); bonds[bondId] = Bond(to, expiry, rdpxAmount); emit BondMinted(to, bondId, expiry, rdpxAmount); }
As it can be seen, expiry is not validated to be greater than block.timestamp. Therefore considering the impact the expiry
must be validated to greater than block.timestamp.
function mint( address to, uint256 expiry, uint256 rdpxAmount ) external onlyRole(MINTER_ROLE) { _whenNotPaused(); require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter"); + require(expiry > block.timestamp, "bond expired"); uint256 bondId = _mintToken(to); bonds[bondId] = Bond(to, expiry, rdpxAmount); emit BondMinted(to, bondId, expiry, rdpxAmount); }
DpxEthToken.sol
, mint()
, burn()
and burnFrom()
should only be called when the contract is not pausedIn DpxEthToken.sol
, mint()
, burn()
and burnFrom()
can be called anytime though the contract has pause and unpause functionality.
File: contracts/dpxETH/DpxEthToken.sol function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { _mint(to, amount); } function burn( uint256 _amount ) public override(ERC20Burnable, IDpxEthToken) onlyRole(BURNER_ROLE) { _burn(_msgSender(), _amount); } function burnFrom( address account, uint256 amount ) public override onlyRole(BURNER_ROLE) { _spendAllowance(account, _msgSender(), amount); _burn(account, amount); }
There are 3 instances of this issue
It is to be noted that _beforeTokenTransfer()
can be called when the contract is not paused. I argue to call the mint and burn functions when the contract is not paused.
In other contracts like RdpxDecayingBonds.sol
mint()
can only be called when the contract is not paused. Similarly this can considered here too.
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { + _whenNotPaused(); _mint(to, amount); } function burn( uint256 _amount ) public override(ERC20Burnable, IDpxEthToken) onlyRole(BURNER_ROLE) { + _whenNotPaused(); _burn(_msgSender(), _amount); } function burnFrom( address account, uint256 amount ) public override onlyRole(BURNER_ROLE) { + _whenNotPaused(); _spendAllowance(account, _msgSender(), amount); _burn(account, amount); }
addToContractWhitelist()
does not check the contract code existence before whitelistingIn PerpetualAtlanticVault.sol
, addToContractWhitelist()
function is used to whitelist contract address. However it does not check the contract code existence before whitlisting it. It is possible that any EOA address or any malicious or phoney address may get whitelisted by admin.
There is 1 instance of this issue:
File: contracts/perp-vault/PerpetualAtlanticVault.sol function addToContractWhitelist( address _contract ) external onlyRole(DEFAULT_ADMIN_ROLE) { _addToContractWhitelist(_contract); }
Check code existence check before whitelisting.
function addToContractWhitelist( address _contract ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_contract.code.lenght != 0, "EOA address can not be whitelisted"); _addToContractWhitelist(_contract); }
RdpxV2Bond.sol
, mint()
should be called when the contract is not pausedIn RdpxV2Bond.sol
, mint()
can be called anytime though the contract has pause and unpause functionality.
File: contracts/core/RdpxV2Bond.sol function mint( address to ) public onlyRole(MINTER_ROLE) returns (uint256 tokenId) { tokenId = _tokenIdCounter.current(); _tokenIdCounter.increment(); _mint(to, tokenId); }
There are 1 instances of this issue
It is to be noted that _beforeTokenTransfer()
can be called when the contract is not paused. I argue to call the mint() when the contract is not paused.
In other contracts like RdpxDecayingBonds.sol
mint()
can only be called when the contract is not paused. Similarly this can considered here too.
function mint( address to ) public onlyRole(MINTER_ROLE) returns (uint256 tokenId) { + _whenNotPaused(); tokenId = _tokenIdCounter.current(); _tokenIdCounter.increment(); _mint(to, tokenId); }
getLpPrice()
in UniV2LiquidityAmo.sol
getLpPrice()
Natspec states Price is in 1e8 Precision
, however this is not correct, The price returned in 1e18.
There is 1 instances of this issue:
File: contracts/amo/UniV2LiquidityAmo.sol /** * @notice Returns the price of a rDPX/ETH Lp token against the alpha token * @dev Price is in 1e8 Precision * @return uint256 LP price **/ function getLpPrice() public view returns (uint256) { return IRdpxEthOracle(addresses.rdpxOracle).getLpPriceInEth(); }
I have confirmed the issue by recheching the RdpxEthOracle.sol
which is given as below,
/// @dev Returns the price of LP in ETH in 1e18 decimals function getLpPriceInEth() external view override returns (uint) {
/** * @notice Returns the price of a rDPX/ETH Lp token against the alpha token - * @dev Price is in 1e8 Precision + * @dev Price is in 1e18 Precision * @return uint256 LP price **/ function getLpPrice() public view returns (uint256) { return IRdpxEthOracle(addresses.rdpxOracle).getLpPriceInEth(); }
deadline
should be passed as an argument instead of hardcoding itIn contracts like UniV2LiquidityAmo.sol
, UniV3LiquidityAmo.sol
and other contracts functions like addLiquidity(), removeLiquidity() and other functions have used deadline. The deadline is hardcoded in contract. block.timestamp can be manipulated by miners/stakers. Therefore hardcoding deadline is not recommended.
Pass the deadline as an argument for all functions using it in contracts.
#0 - c4-pre-sort
2023-09-10T11:40:14Z
bytes032 marked the issue as sufficient quality report
#1 - GalloDaSballo
2023-10-10T11:16:12Z
[L‑01] openzeppelin’s setupRole() is deprecated 7 L
[L‑02] set max limit of slippageTolerance in setSlippageTolerance() 2 I
[L‑03] Calls inside loops that may address DoS 1 -3
[L‑04] Admin may not receive usdc/usdt if blacklisted in case of emergency 1 I
[L‑05] Insufficient validations in setAddresses() can cause both tokenA and tokenB same 2 I
[L‑06] emergencyWithdraw() functionality should be added in UniV3LiquidityAmo.sol like other contracts 1 L
[L‑07] TickMath.sol might revert in solidity version 0.8.19 as used in contracts 1 L
[L‑08] Update solc version and use unchecked in Uniswap related libraries like LiquidityAmounts.sol 1 I
[L‑09] Solmate safeTransferLib.sol functions does not check the codesize of the token address, which may lead to fund loss 1 I
[L‑10] An issue with TransferHelper.sol which is extensively used in UniV3LiquidityAmo.sol 1 I
[L‑11] Do not hard code the contract address, pass as constructor argument in UniV3LiquidityAmo.sol 1 -3, OOS
[L‑12] In RdpxDecayingBonds.sol, mint() does not verify that the bond expiry has passed/expired 1 -3, the caller does
[L‑13] In DpxEthToken.sol, mint(), burn() and burnFrom() should only be called when the contract is not paused 3 L
[L‑14] addToContractWhitelist() does not check the contract code existence before whitelisting 1 NC
[L‑15] In RdpxV2Bond.sol, mint() should be called when the contract is not paused 1 I
[L‑16] Misleading Natspec on getLpPrice() in UniV2LiquidityAmo.sol 1 L
[L‑17] deadline should be passed as an argument instead of hardcoding it contracts L
6L 1NC -9
#2 - c4-judge
2023-10-20T10:21:44Z
GalloDaSballo marked the issue as grade-b