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: 104/189
Findings: 2
Award: $65.42
🌟 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 first depositor can maliciously manipulate the share price by depositing the lowest possible amount (1 wei
) of liquidity and then artificially inflating the total number of assets.
This can inflate the base share price which force next deposits to use the artificially high share price.
Imagine the next scenario (it's simple, but it does its job):
deposit()
.1 wei
assets via deposit()
.transfer()
to the vault to artificially inflate the asset balance without minting new shares.supply == 0 ? assets : assets.mulDivDown(supply, totalVaultCollateral)
, in case of a very high share price, due to totalVaultCollateral > assets * supply
, the returned shares that Bob receives will be drastically less than the expected.The check in line 123
require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");
prevents Bob from receiving 0 shares due to rounding down, though (see this for a similar issue)
Manual analysis
This is a well-known issue, Uniswap and other protocols had similar issues when supply == 0
.
For the first deposit, mint a fixed amount of shares, e.g. 10**decimals()
if (supply == 0) { return 10**decimals; } else { return assets.mulDivDown(supply, totalVaultCollateral); }
ERC4626
#0 - c4-pre-sort
2023-09-07T13:30:46Z
bytes032 marked the issue as duplicate of #863
#1 - c4-pre-sort
2023-09-11T09:10:21Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-18T12:41:47Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-18T12:50:36Z
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
In PerpetualAtlanticVaultLP#L95, the condition shall be an and/&&
so that we do not let the contract to get deployed with 0-addresses values.
emergencyWithdraw
NOTE -> I will work with the generic one, just Ctrl+f
in VSCode.
The function emergencyWithdraw
does loop through the tokens inside tokens
.
function emergencyWithdraw( address[] calldata tokens ) external onlyRole(DEFAULT_ADMIN_ROLE) { IERC20WithBurn token;TODO whenPaused como en el resro 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); }
However, if one of the transfers does revert (e. g. dust attack with a custom token that reverts upon calling transfer
or the contract being blacklisted for whatever reason), then the funds will remain there until the next call with the updated tokens
argument, which in a critical situation could mean losing all of them (thieves front-runs the second call by the admin to emergencyWithdraw
). Consider using a try
/catch
approach so that, even if one transfer does revert, at least some funds will be recovered.
NOTE -> although it seems to be duplicated from bot report L-12, it does only show one occurrence. I put the others here for completeness (I did read the bot report after writing this one)
Consider putting limits to critical state variables to avoid, for example, paying extremely high fees due to a human error or just the admin being malicious. Occurrences:
NOTE -> this submission should be a medium, though, due to the reasons below and there may be more critical situations I did not thought about, but I guess judges will think I am trying to pump the severity, so I put it here
In UniV3LiquidityAmo::swap, the deadline for the function ExactInputSinglePArams
is hard-coded to 2105300114, which is equivalent to Wed Sep 17 2036 21:35:14 GMT+0000, that is, in 13 years. It's bad because 1) blockchain is immutable and this project does not seem to be upgradeable, so once the current Unix timestamp becomes higher such a function will be broken 2) it gives miners and bots a window of oportunities to perform MEV as there is no "real" deadline, so they can store the tx and execute it once it profits them the most or 3) the tx may become idle and be executed at the wrong time (to name some situations). The same applies to UniV3LiquidityAmo#190, which sets the deadline to type(uint256).max
(LMAO), although it does not suffer from 1). Do not hard-code them, do what I say in [L-05]
block.timestamp
, which is modifiable by minersFor setting deadlines, it is better to receive the value from the front-end via an argument instead of relying on block.timestamp
, which may be manipulated by miners to make profits from the caller tx. Consider receiving deadlines via function argument in UniV2LiquidityAMO#L231, UniV2LiquidityAMO#L279, UniV2LiquidityAMO#L342, UniV3LiquidityAmo#190, UniV3LiquidityAmo#250 and UniV3LiquidityAmo#295, which improves UX as the user (or the front-end) knows exactly the deadline, instead of relying on miners who can set a few seconds in the future the current timestamp, which may not be accurate and may harm short-traders.
For occurrences, see the first line of the files in scope. Anyone can copy your code and make their own project equal to yours without giving you any credits. That means, if for any chance they have more traction, user adoption, fundraising rounds or even better devs, then they can kick you out of the market even when they copied your code and you wouldn't be able to bring them to court due to the fact that your code is unlicensed (AKA free). Just MIT or license it somehow
In RdpxDecayingBonds::emergencyWithdraw, the comment for the argument to
is The address to transfer the funds to
, so the funds shall go to such address, but there is a @notice
at the top which says Transfers all funds to msg.sender
. However, only ETH is sent to the to
address and the other assets are sent to the caller (AKA the admin).
function emergencyWithdraw( address[] calldata tokens, bool transferNative, address payable to, uint256 amount, uint256 gas ) external onlyRole(DEFAULT_ADMIN_ROLE) { _whenPaused(); if (transferNative) { (bool success, ) = to.call{ value: amount, gas: gas }(""); <=============== HERE funds sent to `to` require(success, "RdpxReserve: transfer failed"); } IERC20WithBurn token; for (uint256 i = 0; i < tokens.length; i++) { token = IERC20WithBurn(tokens[i]); token.safeTransfer(msg.sender, token.balanceOf(address(this))); } ^============================================== HERE tokens sent the owner with the role DEFAULT_ADMIN_ROLE }
Consider either sending the tokens to the to
as the comment suggests and remove the @notice
or send everything to the caller and remove the to
parameter, as the comments are contrary to each other
In RdpxDecayingBonds::decreaseAmount, the @notice
and the name of the function suggests that the new amount shall be less than the current one. However, it does not check that, indeed, is less nor it is different, being misleading and doing unnecessary opcodes respectively. Consider changing the function corpse to
_whenNotPaused(); uint256 temp = bonds[bondId].rdpxAmount; // sload require(amount < temp, "..."); bonds[bondId].rdpxAmount = amount; // sstore
or changing the comment and the function name if your intention for the function is to behave like a setter.
NOTE -> I will put it as gas too
In the constructor, the contract does increment ˋ_tokenIdCounterˋ by 1, so the call to ˋ_mintTokenˋ. will mint the bond with index 2 instead of 1. Consider removing the line 63 in the constructor so that the token id start at 1 when calling ˋ_mintTokenˋ for the first time.
constructor( string memory _name, string memory _symbol ) ERC721(_name, _symbol) { // Grant the minter role and admin role to deployer _setupRole(MINTER_ROLE, msg.sender); _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); _tokenIdCounter.increment(); <============================ HERE }
In RdpxV2Core#L374 and RdpxV2Core#L384 should be an AMO
instead of a AMO
. There is another typo here, where it says tolernce
instead of tolerance
.
The functions emergencyWithdraw
are supposed to be called when the contract is paused as seen here and here. However, in UniV2LiquidityAmo it does not nor it is Pausable
. Consider putting at the top _whenPaused();
and making the contract Pausable
, as the others.
_assetSymbol
In RdpxV2core#addAssetTotokenReserves there is a check for the new asset to be indeed new. However, there is no check for _assetSymbol
. Because it is being done for _asset
, consider doing the same for _assetSymbol
.
NOTE -> duplicated from bot report N-55, I put it here because this is a different occurrence and I did read the report afterwards
The function RdpxV2core#addAssetTotokenReserves
shall be RdpxV2core#addAssetToTokenReserves
, with ...token...
being ...Token...
. The same applies to the event LogAssetAddedTotokenReserves
Given that the constructor
in DpxEthToken is giving many roles to the caller, he should receive the BURNER_ROLE
too.
#0 - c4-pre-sort
2023-09-10T11:50:48Z
bytes032 marked the issue as sufficient quality report
#1 - GalloDaSballo
2023-10-10T11:39:22Z
Wrong conditional L Possible failure in emergencyWithdraw I Many setters does not have upper/lower caps -3 Function broken due to hard-coding swap deadline L It's best practice to receive deadlines directly from the front-end instead of relying on block.timestamp, which is modifiable by miners -3 Unlicensed code I Comment does not adhere to implementation L RdpxDecayingBonds::_tokenIdCounter starts at instead of I Typo I Consistency between functions L Lack of checks for duplicated _assetSymbol L Camel case issues I Completeness in DpxEthToken I
#2 - c4-judge
2023-10-20T10:21:56Z
GalloDaSballo marked the issue as grade-b