Platform: Code4rena
Start Date: 11/12/2023
Pot Size: $90,500 USDC
Total HM: 29
Participants: 127
Period: 17 days
Judge: TrungOre
Total Solo HM: 4
Id: 310
League: ETH
Rank: 111/127
Findings: 1
Award: $20.82
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: SBSecurity
Also found by: 0xaltego, 0xbepresent, Aymen0909, Bauchibred, Cosine, EVDoc, EloiManuel, HighDuty, Sathish9098, Tendency, Timeless, ZanyBonzy, beber89, deliriusz, ether_sky, grearlake, hals, klau5, lsaudit, nadin, rvierdiiev, tsvetanovv
20.8157 USDC - $20.82
votingPeriod
is over to usersTake a look at votingPeriod()
function votingPeriod() public pure override returns (uint256) { return 2425847; // ~1 year with 1 block every 13s }
As seen, this function is used to return the voting period, evidently this function returns 2425847
, issue is that this should be 1 year but protocol makes the assumption that 1 block is mined every 13s, now this makes protocol off over a month on the ethereum mainnet, this is cause 2425847
amounts to around 336 days which is 1 month of the intended value.
Now to worsen this, is the fact that protocol is actually going to be deployed not only on the Ethereum mainnet but even Arbitrum type L2s as clearly stated here, what this now leads to is having a way off number for the voting period depending on what chain this gets deployed, using Arbitrum as a case study, from their official docs, one can see that a single Ethereum block includes multiple Arbitrum blocks within it which leads to this value been shorter than necessary in this case, and if takes it shorter on a different Arbitrum like chain then the same case would be made.
Protocol can't rightly relay if the votingPeriod
is over to users, i.e after the action is ready in the timelock, probably limiting users engagements in voting since they might assume voting's over when it's not.
Do not hardcode the votingPeriod
value for all chains and also it shouldn't be 2425847
for the Ethereum mainnet as this is one month off than the original value.
Take a look at createTerm()
function createTerm( address implementation, LendingTerm.LendingTermParams calldata params ) external returns (address) { require( implementations[implementation], "LendingTermOnboarding: invalid implementation" ); // must be an ERC20 (maybe, at least it prevents dumb input mistakes) (bool success, bytes memory returned) = params.collateralToken.call( abi.encodeWithSelector(IERC20.totalSupply.selector) ); require( success && returned.length == 32, "LendingTermOnboarding: invalid collateralToken" ); require( params.maxDebtPerCollateralToken != 0, // must be able to mint non-zero debt "LendingTermOnboarding: invalid maxDebtPerCollateralToken" ); require( //@audit params.interestRate < 1e18, // interest rate [0, 100[% APR "LendingTermOnboarding: invalid interestRate" ); require( // 31557601 comes from the constant LendingTerm.YEAR() + 1 params.maxDelayBetweenPartialRepay < 31557601, // periodic payment every [0, 1 year] "LendingTermOnboarding: invalid maxDelayBetweenPartialRepay" ); require( params.minPartialRepayPercent < 1e18, // periodic payment sizes [0, 100[% "LendingTermOnboarding: invalid minPartialRepayPercent" ); require( //@audit params.openingFee <= 0.1e18, // open fee expected [0, 10]% "LendingTermOnboarding: invalid openingFee" ); require( params.hardCap != 0, // non-zero hardcap "LendingTermOnboarding: invalid hardCap" ); address term = Clones.clone(implementation); LendingTerm(term).initialize( address(core()), LendingTerm.LendingTermReferences({ profitManager: profitManager, guildToken: guildToken, auctionHouse: auctionHouse, creditMinter: creditMinter, creditToken: creditToken }), params ); created[term] = block.timestamp; emit TermCreated(block.timestamp, implementation, term, params); return term; }
As seen with the help of the @audit tags, unlike with the fees where the limits are acceptable, i.e 0 and 10%, in regards to the interest rate it must be less than 100% which is an unintended implementation as commented by the docs.
Broken logic/ Wrong Docs
Change this:
require( //@audit params.interestRate < 1e18, // interest rate [0, 100[% APR "LendingTermOnboarding: invalid interestRate" );
to:
require( //@audit params.interestRate <+ 1e18, // interest rate [0, 100[% APR "LendingTermOnboarding: invalid interestRate" );
canExceed...
implemented functionalities should have a somewhat second cap limit attached to themUsing this search command: https://github.com/search?q=repo%3Acode-423n4%2F2023-12-ethereumcreditguild%20canexceed&type=code, i.e checking for instances of canExceed...
we can see that protocol uses this to give out special treatment to some addresses in different logic, issue with this is that in the case where the list grows too much, be it that of delegates in the ERC20MultiVotes.sol
contract or gueages in the ERC20Guages.sol
contract users then functionality would be massively affected
For example in regards to the multivotes system The identified issue arises when accounts, which are allowed to surpass the canContractExceedMaxDelegates
global limit, accumulate an excessively large number of delegates. These accounts are not restricted by the maxDelegates
variable, leading to potential problems. Specifically, when new delegates are added, there's no check to prevent an out-of-gas (OOG) exception during iterations over an extensive delegate list. Although there is a provision to remove delegates individually, it requires that the delegates have no allocated votes. In scenarios where a delegator has numerous delegates, each with allocated votes, both the burn and transfer functions for the delegator are at risk of a denial-of-service (DoS). This is because the operations attempt to iterate through the entire delegate list to free votes, potentially triggering an OOG exception, thereby causing the burn and transfer functions to consistently revert.
Multiple functionalities are at risk of falling into a DoS if the list grows too much and the delegates don't have free votes to allow the delegator to remove their delegates individually.
Maybe an idea to fix this would be to set a second cap limit on the number of delegates that a delegator that is allowed to skip the first limit can delegate their votes to.
Take a look at updateTimelock()
function updateTimelock( address newTimelock ) external onlyCoreRole(CoreRoles.GOVERNOR) { _updateTimelock(newTimelock); } function _updateTimelock(address newTimelock) private { emit TimelockChange(timelock, newTimelock); timelock = newTimelock; }
As seen the function is used to update the timelock the veto governor can cancel from, but there are no checks to ensure that the ew value that's been set is not equal to the previous one.
NB: This is the same for all setter/updater instances
Low
Implement checks that the new value that's to be set is not the same as what was previously there.
_replenishBuffer()
Take a look at _replenishBuffer()
function _replenishBuffer(uint256 amount) internal { uint256 newBuffer = buffer(); uint256 _bufferCap = bufferCap; // gas optimization // Logic to calculate the actual amount replenished if (newBuffer == _bufferCap) { return; } uint32 blockTimestamp = block.timestamp.safeCastTo32(); uint224 newBufferStored = Math.min(newBuffer + amount, _bufferCap).safeCastTo224(); lastBufferUsedTime = blockTimestamp; bufferStored = newBufferStored; // @audit Inaccurate event emission emit BufferReplenished(amount, bufferStored); }
The event BufferReplenished(amount, bufferStored)
emits the amount
parameter as the amount replenished. However, the actual replenished amount may be less than amount
if adding amount
to the current buffer exceeds the buffer cap. The function correctly calculates newBufferStored
as the minimum of newBuffer + amount
and _bufferCap
, but the event does not reflect this calculation.
The _replenishBuffer
function in the RateLimitedV2
contract emits an event that inaccurately represents the amount of buffer replenished. This inaccuracy arises because the event emission does not account for cases where the actual replenished amount is less than the requested amount. This could lead to misleading information being recorded on the blockchain, causing confusion and potential misinterpretation of the contract's state and actions.
Modify the event emission to accurately reflect the actual amount replenished. This can be done by calculating the difference between newBufferStored
and the buffer size before replenishment.
mint
and burn
are not checked could lead toUsing these search commands:
We can see that there are multiple instances of where the protcol mints to or burns from an address, but the return values of most of these operations are not checked, for e.g in ERC20RebaseDistributor.sol
Since the return value of mint
and burn
are not checked could lead to instances where the minting is not successful but protocol assumes they are, probably leading to users missing out on their tokens or protocol leaking value in an attempt to burn tokens that doesn't get successful
Check the return value of these executions
nonRebasingSupply()
should be made more efficientThe nonRebasingSupply
function in a given smart contract is designed to calculate the non-rebasing supply of tokens by subtracting the rebasingSupply
from the totalSupply
. However, there is a potential issue in the comparison logic used to handle rounding errors and prevent underflows.
function nonRebasingSupply() external view virtual returns (uint256) { uint256 _totalSupply = totalSupply(); uint256 _rebasingSupply = rebasingSupply(); // Comparison to handle potential underflow due to rounding errors if (_rebasingSupply > _totalSupply) { return 0; } else { return _totalSupply - _rebasingSupply; } }
Currently, the function checks if _rebasingSupply
is greater than _totalSupply
to prevent underflow when subtracting. However, there is an audit note suggesting that this comparison should use a ">=" check. This is based on the reasoning that if _rebasingSupply
equals _totalSupply
, the subtraction would result in zero anyway, making the entire operation redundant.
Failing to implement the suggested ">=" check could lead to unnecessary subtraction operations when _rebasingSupply
and _totalSupply
are equal. While this won't result in incorrect calculations or critical issues, optimizing the check can improve the function's efficiency and clarity, ensuring that it only performs subtraction when necessary.
Update the comparison in the if
statement to use ">=" instead of ">". This change will ensure that the function directly returns 0
when _rebasingSupply
is equal to or greater than _totalSupply
, thus avoiding an unnecessary subtraction operation in these cases. This adjustment not only aligns with logical expectations but also optimizes the function for cases where both values are equal.
#0 - c4-pre-sort
2024-01-05T18:00:59Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - Trumpero
2024-01-31T08:10:28Z
Q-01: low (dup of #816) Q-02: info Q-03: info Q-04: NC Q-05: NC Q-06: bot finding (D-19) Q-07: NC
#2 - Trumpero
2024-01-31T11:56:09Z
#3 - c4-judge
2024-01-31T11:56:29Z
Trumpero marked the issue as grade-b