Platform: Code4rena
Start Date: 31/01/2023
Pot Size: $90,500 USDC
Total HM: 47
Participants: 169
Period: 7 days
Judge: LSDan
Total Solo HM: 9
Id: 211
League: ETH
Rank: 94/169
Findings: 3
Award: $54.34
π Selected for report: 0
π Solo Findings: 0
π Selected for report: 0xNineDec
Also found by: 0xBeirao, 0xNazgul, 0xRajkumar, Blockian, Breeje, CRYP70, Josiah, KIntern_NA, MyFDsYours, Qeew, RaymondFam, Ruhum, UdarTeam, chaduke, giovannidisiena, gjaldon, immeas, koxuan, nadin, peanuts, rbserver, rvi0x, savi0ur
14.2839 USDC - $14.28
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L294-L301
This is a common attack vector involving shares based liquidity pool contracts. 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.
Apparently, the first depositor of an ERC4626 vault can maliciously manipulate the share price by depositing the lowest possible amount with 1 wei of liquidity and then artificially inflating ERC4626.totalAssets.
This can inflate the base share price to as high as 1:1e18, forcing all subsequent deposits to be dictated by this share price as a base. Additionally, due to rounding down, if this malicious initial deposit were to front-run someone else depositing, this depositor would end up receiving 0 shares and losing his deposited assets.
ERC4626 vault share price can be maliciously inflated on the initial deposit, leading to the next depositor losing assets due to precision issues.
Here is a typical exploit scenario where:
A vault is using DAI as its underlying asset.
deposit()
.Note: As denoted in the code block below, shares are determined via its return statement, supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), Math.Rounding.Down). In the event of a very high share price, due to totalAssets()
very much larger than assets * supply
, shares will be returned 0.
function convertToShares(uint256 assets) public view returns (uint256) { uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), Math.Rounding.Down); }
Manual inspection
Consider sending the first 1000 shares to address 0, a mitigation approach adopted by the Uniswap V2 protocol when supply == 0.
Additionally, the protocol could look into implementing slippage protection to further mitigate the situations.
#0 - c4-judge
2023-02-16T03:31:00Z
dmvt marked the issue as duplicate of #15
#1 - c4-sponsor
2023-02-18T11:54:50Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:38:31Z
dmvt marked the issue as partial-50
#3 - c4-judge
2023-02-23T01:06:13Z
dmvt changed the severity to 3 (High Risk)
#4 - c4-judge
2023-03-01T00:39:40Z
dmvt marked the issue as satisfactory
π Selected for report: csanuragjain
Also found by: 0xNazgul, 0xNineDec, 0xSmartContract, 0xdeadbeef0x, Bauer, Deivitto, Josiah, KIntern_NA, RaymondFam, Rolezn, UdarTeam, Viktor_Cortess, btk, joestakey, koxuan, pavankv, rbserver, rvi0x
4.5833 USDC - $4.58
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L134-L198
When depositing assets to Vault.sol via deposit() or mint(), the underlying ERC20 token amount transferred to the contract proportionately determines the minted amount of vault shares to the receiver. However, if the ERC20 token entailed is deflationary, it could lead to accounting errors resulting in the last batch of depositors (receivers) unable to withdraw their funds due to insufficient contract balance.
As can be seen in the code blocks below, no measure has been made to either stem or cater for fee-on-transfer tokens.
function deposit(uint256 assets, address receiver) public nonReentrant whenNotPaused syncFeeCheckpoint returns (uint256 shares) { if (receiver == address(0)) revert InvalidReceiver(); uint256 feeShares = convertToShares( assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down) ); shares = convertToShares(assets) - feeShares; if (feeShares > 0) _mint(feeRecipient, feeShares); _mint(receiver, shares); asset.safeTransferFrom(msg.sender, address(this), assets); adapter.deposit(assets, address(this)); emit Deposit(msg.sender, receiver, assets, shares); } function mint(uint256 shares, address receiver) public nonReentrant whenNotPaused syncFeeCheckpoint returns (uint256 assets) { if (receiver == address(0)) revert InvalidReceiver(); uint256 depositFee = uint256(fees.deposit); uint256 feeShares = shares.mulDiv( depositFee, 1e18 - depositFee, Math.Rounding.Down ); assets = convertToAssets(shares + feeShares); if (feeShares > 0) _mint(feeRecipient, feeShares); _mint(receiver, shares); asset.safeTransferFrom(msg.sender, address(this), assets); adapter.deposit(assets, address(this)); emit Deposit(msg.sender, receiver, assets, shares); }
Manual inspection
Consider:
#0 - c4-judge
2023-02-16T07:07:50Z
dmvt marked the issue as duplicate of #44
#1 - c4-sponsor
2023-02-18T11:49:07Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:44:02Z
dmvt marked the issue as partial-50
π Selected for report: IllIllI
Also found by: 0x3b, 0xAgro, 0xBeirao, 0xMirce, 0xNineDec, 0xRobocop, 0xSmartContract, 0xTraub, 0xWeiss, 2997ms, 41i3xn, Awesome, Aymen0909, Bauer, Bnke0x0, Breeje, Cryptor, DadeKuma, Deathstore, Deekshith99, DevABDee, DevTimSch, Dewaxindo, Diana, Ermaniwe, Guild_3, H0, IceBear, Inspectah, JDeryl, Kaiziron, Kaysoft, Kenshin, Mukund, Praise, RaymondFam, Rickard, Rolezn, Ruhum, Sathish9098, SkyWalkerMan, SleepingBugs, UdarTeam, Udsen, Walter, aashar, adeolu, apvlki, arialblack14, ast3ros, btk, chaduke, chandkommanaboyina, chrisdior4, climber2002, codetilda, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, descharre, dharma09, doublesharp, eccentricexit, ethernomad, fs0c, georgits, halden, hansfriese, hashminer0725, immeas, lukris02, luxartvinsec, matrix_0wl, merlin, mookimgo, mrpathfindr, nadin, olegthegoat, pavankv, rbserver, rebase, savi0ur, sayan, scokaf, seeu, shark, simon135, tnevler, tsvetanovv, ulqiorra, ustas, waldenyan20, y1cunhui, yongskiws, yosuke
35.4779 USDC - $35.48
Immutable instances should be parameterized at the constructor instead of getting them directly assigned in the state variable declaration.
Here are some of the instances entailed:
File: VaultController.sol#L36-L40
bytes32 public immutable VAULT = "Vault"; bytes32 public immutable ADAPTER = "Adapter"; bytes32 public immutable STRATEGY = "Strategy"; bytes32 public immutable STAKING = "Staking"; bytes4 internal immutable DEPLOY_SIG = bytes4(keccak256("deploy(bytes32,bytes32,bytes)"));
For some source-units the compiler version pragma is very unspecific, i.e. ^0.8.15. While this often makes sense for libraries to allow them to be included with multiple different versions of an application, it may be a security risk for the actual application implementation itself. A known vulnerable compiler version may accidentally be selected or security tools might fall-back to an older compiler version ending up actually checking a different EVM compilation that is ultimately deployed on the blockchain.
Avoid floating pragmas where possible. It is highly recommend pinning a concrete compiler version (latest without security issues) in at least the top-level βdeployedβ contracts to make it unambiguous which compiler version is being used. Rule of thumb: a flattened source-unit should have at least one non-floating concrete solidity compiler version pragma.
The protocol adopts version 0.8.15 on writing contracts. For better security, it is best practice to use the latest Solidity version, 0.8.17.
Security fix list in the versions can be found in the link below:
https://github.com/ethereum/solidity/blob/develop/Changelog.md
Solidity contracts can use a special form of comments, i.e., the Ethereum Natural Language Specification Format (NatSpec) to provide rich documentation for functions, return variables and more. Please visit the following link for further details:
https://docs.soliditylang.org/en/v0.8.16/natspec-format.html
For instance, it will be of added values to the users and developers if:
deploy()
in CloneFactory.sol- * @notice Ownes contracts in the vault ecosystem to allow for easy ownership changes. + * @notice Owner contracts in the vault ecosystem to allow for easy ownership changes.
- * Is used by `VaultController` to check if a target is a registerd clone. + * Is used by `VaultController` to check if a target is a registered clone.
- /// @notice The amount of assets that are free to be withdrawn from the yVault after locked profts. + /// @notice The amount of assets that are free to be withdrawn from the yVault after locked profits.
- * @dev This function is the one stop solution to create a new vault with all necessary admin functions or auxiliery contracts. + * @dev This function is the one stop solution to create a new vault with all necessary admin functions or auxiliary contracts.
Functions with named returns should not have a return statement to avoid unnecessary confusion.
For instance, the following function may be refactored as:
function execute(address target, bytes calldata callData) external onlyOwner returns (bool success, bytes memory returndata) { - return target.call(callData); + (success, returndata) = target.call(callData); }
Although only the owner can call addClone()
in CloneRegistry.sol, it does not prevent accidental addition of duplicate clones.
Consider having the function refactored as follows to ensure all clones pushed into the arrays are unique:
File: CloneRegistry.sol#L41-L51
+ error CloneAlreadyAdded(); function addClone( bytes32 templateCategory, bytes32 templateId, address clone ) external onlyOwner { + if (cloneExists[clone]) revert CloneAlreadyAdded(); cloneExists[clone] = true; clones[templateCategory][templateId].push(clone); allClones.push(clone); emit CloneAdded(clone); }
delete
to clear variablesdelete a
assigns the initial value for the type to a
. i.e. for integers it is equivalent to a = 0
, but it can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements reset. For structs, it assigns a struct with all members reset. Similarly, it can also be used to set an address to zero address or a boolean to false. It has no effect on whole mappings though (as the keys of mappings may be arbitrary and are generally unknown). However, individual keys and what they map to can be deleted: If a
is a mapping, then delete a[x]
will delete the value stored at x.
The delete key better conveys the intention and is also more idiomatic.
For instance, the a = false
instance below may be refactored as follows:
File: TemplateRegistry.sol#L75
- template.endorsed = false; + delete template.endorsed;
According to Solidity's Style Guide below:
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
In order to help readers identify which functions they can call, and find the constructor and fallback definitions more easily, functions should be grouped according to their visibility and ordered in the following manner:
constructor, receive function (if exists), fallback function (if exists), external, public, internal, private
And, within a grouping, place the view and pure functions last.
Additionally, inside each contract, library or interface, use the following order:
type declarations, state variables, events, modifiers, functions
Consider adhering to the above guidelines for all contract instances entailed.
Lines in source code are typically limited to 80 characters, but itβs reasonable to stretch beyond this limit when need be as monitor screens theses days are comparatively larger. Considering the files will most likely reside in GitHub that will have a scroll bar automatically kick in when the length is over 164 characters, all code lines and comments should be split when/before hitting this length. Keep line width to max 120 characters for better readability where possible.
Here are some of the instances entailed:
File: AdapterBase.sol#L6 File: MultiRewardStaking.sol#L7
Some contracts have interface(s) or libraries showing up in its/their entirety at the top/bottom of the contract facilitating an ease of references on the same file page. This has, in certain instances, made the already large contract size to house an excessive code base. Additionally, it might create difficulty locating them when attempting to cross reference the specific interfaces embedded elsewhere but not saved into a particular .sol file.
Consider saving the interfaces and libraries entailed respectively, and having them imported like all other files.
Here are some of the instances entailed:
#0 - c4-judge
2023-02-28T14:59:59Z
dmvt marked the issue as grade-b