Platform: Code4rena
Start Date: 31/03/2022
Pot Size: $75,000 USDC
Total HM: 7
Participants: 42
Period: 7 days
Judge: Jack the Pug
Total Solo HM: 5
Id: 102
League: ETH
Rank: 8/42
Findings: 2
Award: $586.89
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rayn
Also found by: 0xDjango, 0xkatana, 0xkowloon, BouSalman, CertoraInc, Dravee, Funen, Hawkeye, IllIllI, Jujic, Kenshin, Kthere, Meta0xNull, Sleepy, TerrierLover, async, aysha, berndartmueller, catchup, cccz, cmichel, csanuragjain, danb, defsec, georgypetrov, hake, hubble, kenta, kyliek, pauliax, rfa, robee, sahar, shenwilly, teryanarmen
137.8903 USDC - $137.89
https://github.com/code-423n4/2022-03-volt/blob/main/contracts/core/Permissions.sol
The onlyGovernor() modifier is used on contracts to prevent unauthorized accounts from calling restricted functions. Once an account is considered trusted, it is allowed to add and remove accounts by calling permission functions as they see fit. However, if any single account has its private keys compromised or decides to become malicious on their own, they can remove all other trusted accounts. As a result, he/she is effectively able to take over the trusted group that controls all restricted functions in the protocol.
Manual code review.
The Governor account should point to a multisig managed by the Volt team or by a community DAO.
#0 - ElliotFriedman
2022-04-07T18:42:59Z
The governor is a trusted role in the system.
#1 - jack-the-pug
2022-04-20T12:47:53Z
I think this is a legit concern, the governor role is too powerful to be held by an EOA: deployCore.sh#L17
I'm afraid this is a bit out of scope though. Thus, I will downgrade to QA
, but this is a noteworthy issue.
#2 - JeeberC4
2022-05-03T22:12:52Z
Generating QA Report as warden did not have one and judge downgraded issue. Preserving original title: A Single Malicious Trusted Account Can Takeover the Protocol
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0xNazgul, 0xkatana, 0xkowloon, CertoraInc, Dravee, Funen, Hawkeye, Jujic, Kenshin, Meta0xNull, Sleepy, TerrierLover, catchup, csanuragjain, defsec, georgypetrov, kenta, okkothejawa, rayn, rfa, robee, saian, samruna
448.9974 USDC - $449.00
#1. Long Revert Strings
##Impact Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition has been met.
There are several other places throughout the codebase where the same optimization can be used.
##Tools
https://planetcalc.com/9029/ ##Recommended Mitigation Steps Shorten the revert strings to fit in 32 bytes.
For the arithmetic operations that will never over/underflow, using the unchecked directive (Solidity v0.8 has default overflow/underflow checks) can save some gas from the unnecessary internal over/underflow checks.
int256 delta = int128(currentMonth) - int128(previousMonth);
require( amountVoltOut >= minVoltAmountOut, "PegStabilityModule: Mint not enough out" ); underlyingToken.safeTransferFrom( msg.sender, address(pcvDeposit), amountIn ); uint256 amountFeiToTransfer = Math.min( volt().balanceOf(address(this)), amountVoltOut ); uint256 amountFeiToMint = amountVoltOut - amountFeiToTransfer;
require(amount <= newBuffer, "MultiRateLimited: rate limit hit"); rateLimitPerAddress[rateLimitedAddress].bufferStored = uint112( newBuffer - amount );
require(usedAmount <= newBuffer, "RateLimited: rate limit hit"); bufferStored = newBuffer - usedAmount;
function remainingTime() public view returns (uint256) { return duration - timeSinceStart(); // duration always >= timeSinceStart which is on [0,d] }
uint256 timePassed = block.timestamp - startTime; // block timestamp always >= startTime
##Recommended Mitigation Steps Consider using 'unchecked' where it is safe to do so.
Custom errors are cheaper than revert strings.
Source: https://blog.soliditylang.org/2021/04/21/custom-errors/: Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them. Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).
Replace revert strings with custom errors.
Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks are available for free. The advantages of versions 0.8.* over <0.8.0 are: • Safemath by default from 0.8.0 (can be more gas efficient than library based safemath.) • Low level inliner : from 0.8.2, leads to cheaper runtime gas. Especially relevant when the contract has small functions. For example, OpenZeppelin libraries typically have a lot of small helper functions and if they are not inlined, they cost an additional 20 to 40 gas because of 2 extra jump instructions and additional stack operations needed for function calls. • Optimizer improvements in packed structs: Before 0.8.3, storing packed structs, in some cases used an additional storage read operation. After EIP-2929, if the slot was already cold, this means unnecessary stack operations and extra deploy time costs. However, if the slot was already warm, this means additional cost of 100 gas alongside the same unnecessary stack operations and extra deploy time costs. • Custom errors from 0.8.4, leads to cheaper deploy time cost and run time cost. Note: the run time cost is only relevant when the revert condition is met. In short, replace revert strings by custom errors. https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/pcv/compound/CompoundPCVDepositBase.sol#L2
Consider to upgrade pragma to at least 0.8.4.
Where possible, use equivalent function parameters or local variables in event emits instead of state variables to prevent expensive SLOADs. Post-Berlin, SLOADs on state variables accessed first-time in a transaction increased from 800 gas to 2100, which is a 2.5x increase.
function updateScalingPriceOracle(IScalingPriceOracle newScalingPriceOracle) external override onlyOwner { IScalingPriceOracle oldScalingPriceOracle = scalingPriceOracle; scalingPriceOracle = newScalingPriceOracle; emit ScalingPriceOracleUpdate( oldScalingPriceOracle, newScalingPriceOracle ); }
Impact Increase gas costs on all onlyAdmin operations Proof of Concept This variables are marked as constant:
https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/core/Permissions.sol#L10-L15 This results in the keccak operation being performed whenever the variable is used, increasing gas costs relative to just storing the output hash. Changing to immutable will only perform hashing on contract deployment which will save gas. See: ethereum/solidity#9232 (comment) Recommended Mitigation Steps Change the variable to be immutable rather than constant
Gas savings
Solidity 0.8.10 has a useful change which reduced gas costs of external calls which expect a return value: https://blog.soliditylang.org/2021/11/09/solidity-0.8.10-release-announcement/
Code Generator: Skip existence check for external contract if return data is expected. In this case, the ABI decoder will revert if the contract does not exist
LIFI protocol is using 0.8.6: Updating to the newer version of solc will allow contracts to take advantage of these lower costs for external calls.
Update to 0.8.10+
Impact Instead of using operator && on single require check using double require check can save more gas: Proof of Concept https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/volt/Volt.sol#L90-L93
require( recoveredAddress != address(0) && recoveredAddress == owner, "Fei: INVALID_SIGNATURE" );
Recommended Mitigation Steps Change to:
require(recoveredAddress != address(0),"Fei: ZERO ADDRESS" ); require( recoveredAddress == owner, "Fei: INVALID_SIGNATURE" );
Lower than uint256 size storage instance variables are actually less gas efficient. E.g. using uint32 does not give any efficiency, actually, it is the opposite as EVM operates on default of 256-bit values so uint32 is more expensive in this case as it needs a conversion. It only gives improvements in cases where you can pack variables together, e.g. structs.
Consider to review all uint types. Change them with uint256 If the integer is not necessary to present with uint32.
Impact
In the Volt.sol
, change abi.encode to abi.encodePacked can save gas.
https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/volt/Volt.sol#L26
https://github.com/ConnorBlockchain/Solidity-Encode-Gas-Comparison
function timeSinceStart() public view returns (uint256) { if (!isTimeStarted()) { return 0; // uninitialized } uint256 _duration = duration; uint256 timePassed = block.timestamp - startTime; return timePassed > _duration ? _duration : timePassed; }
Change to:
function timeSinceStart() public view returns (uint256) { if (isTimeStarted()) { uint256 _duration = duration; uint256 timePassed = block.timestamp - startTime; return timePassed > _duration ? _duration : timePassed; }
constructor() { // Appointed as a governor so guardian can have indirect access to revoke ability _setupGovernor(address(this)); _setRoleAdmin(MINTER_ROLE, GOVERN_ROLE); _setRoleAdmin(BURNER_ROLE, GOVERN_ROLE); _setRoleAdmin(PCV_CONTROLLER_ROLE, GOVERN_ROLE); _setRoleAdmin(GOVERN_ROLE, GOVERN_ROLE); _setRoleAdmin(GUARDIAN_ROLE, GOVERN_ROLE); }
By default, the admin role for all roles is GOVERN_ROLE. Therefore, _setRoleAdmin(***_ROLE, GOVERN_ROLE); is redundant. Removing it will make the code simpler and save some gas. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/783ac759a902a7b4a218c2d026a77e6a26b6c42d/contracts/access/AccessControl.sol#L40-L43
DEFAULT_ADMIN_ROLE
, which meansRemove the redundant code.