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: 5/127
Findings: 7
Award: $4,366.36
π Selected for report: 2
π Solo Findings: 0
1920.354 USDC - $1,920.35
The AuctionHouse
contract implements a Dutch auction mechanism to recover debt from collateral. However, there is no check for sequencer uptime, which could lead to auctions failing or executing at unfavorable prices.
The current deployment parameters allow auctions to succeed without a loss to the protocol for a duration of 10m 50s. If there's no bid on the auction after this period, the protocol has no other option but to take a loss or forgive the loan. This could have serious consequences in the event of a network outage, as any loss results in the slashing of all users with weight on the term.
Network outages and large reorgs happen with relative frequency. For instance, Arbitrum suffered an hour-long outage just two weeks ago (source).
Consider the following scenario:
midPoint
and before the auction ends) or forgive the loan, both leading to the complete slashing of all users with weight on the term.Manual review
To mitigate this issue, consider integrating an external uptime feed such as Chainlink's L2 Sequencer Feeds. This would allow the contract to invalidate an auction if the sequencer was ever offline during its duration. Alternatively, implement a mechanism to restart an auction if it has received no bids.
Other
#0 - c4-pre-sort
2024-01-03T17:27:00Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-03T17:27:04Z
0xSorryNotSorry marked the issue as primary issue
#2 - eswak
2024-01-18T16:53:00Z
Acknowledging this, we definitely don't want to add a dependency on an oracle to run the liquidations, so maybe another fix would be to define auction durations in number of blocks, but I think basing auctions on time is semantically correct because it depends on market conditions (that are based on time) and when the sequencer resumes the conditions that triggered the auction might not hold anymore. The forgive() path at the end of the auction could be used to unstuck the situation at the smart contract level, and the governance can organize an orderly fix of the situation when the sequencer resumes. Given the likelihood I think it should be low severity, especially since we know auctions have to be longer on L2s than on mainnet (liquidity potentially needs to bridge, etc), so the chance that a downtime large enough relative to the auction duration will happen is pretty low.
#3 - c4-sponsor
2024-01-18T16:53:05Z
eswak (sponsor) acknowledged
#4 - c4-sponsor
2024-01-18T16:53:09Z
eswak marked the issue as disagree with severity
#5 - Trumpero
2024-01-30T16:58:25Z
Imo, if an auction is still active when the sequencer is down, it may cause a loss of assets (collateral) for the borrower. Although governance's measures can help mitigate the situation when the sequencer resumes, loss and slashing in this lending term will be inevitable in such cases. So I think medium severity is appropriate for this issue, but I'm open to other feedback.
#6 - c4-judge
2024-01-30T16:58:31Z
Trumpero marked the issue as satisfactory
#7 - c4-judge
2024-01-31T13:21:16Z
Trumpero marked the issue as selected for report
1920.354 USDC - $1,920.35
med
From the contest documentation:
[The GUARDIAN role] is expected to be able to freeze new usage of the protocol, but not prevent users from withdrawing their funds.
However, due to the whenNotPaused
modifier in the RateLimitedMinter.mint()
function, users who have CREDIT
tokens staked on a gauge with a pending guildReward
will not be able to withdraw their funds. This is because the SurplusGuildMinter.unstake()
function attempts to mint rewards through the RateLimitedMinter
in the getRewards()
function prior to unstaking. If the protocol is paused, this step will fail, causing the transaction to revert and preventing users from withdrawing their funds.
Consider a scenario where Alice has CREDIT
tokens staked on a gauge with a pending guildReward
. Then, the protocol is paused and Alice attempts to unstake her tokens by calling the SurplusGuildMinter.unstake()
function, which in turn calls the getRewards()
function. This function attempts to mint rewards through the RateLimitedMinter.mint()
function. However, because the protocol is paused, this function cannot be executed, causing the transaction to revert and preventing Alice from unstaking and withdrawing her funds.
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/rate-limits/RateLimitedMinter.sol#L52 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SurplusGuildMinter.sol#L158-L163 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SurplusGuildMinter.sol#L259
Manual review
Provide an emergencyWithdraw
method allowing users to withdraw their funds while foregoing rewards when the protocol is paused. This change should be carefully reviewed and tested to ensure it does not introduce other security risks.
Other
#0 - c4-pre-sort
2024-01-03T17:35:03Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-03T17:35:07Z
0xSorryNotSorry marked the issue as primary issue
#2 - eswak
2024-01-18T15:48:49Z
Very nice catch, thanks a lot for reporting :)
#3 - c4-sponsor
2024-01-18T15:48:52Z
eswak (sponsor) confirmed
#4 - c4-judge
2024-01-19T04:36:34Z
Trumpero marked the issue as satisfactory
#5 - c4-judge
2024-01-31T13:18:20Z
Trumpero marked the issue as selected for report
π Selected for report: carrotsmuggler
Also found by: 3docSec, EV_om, PENGUN, SpicyMeatball, TheSchnilch, ether_sky, falconhoof, santipu_
117.7563 USDC - $117.76
The totalBorrowedCredit
function in the ProfitManager
contract may revert if the creditMultiplier
is low. This is because redeemableCredit
is scaled by the creditMultiplier
, but targetTotalSupply
isn't. This can potentially brick core protocol functionality as no one would be able to unstake or borrow.
/// @notice returns the sum of all borrowed CREDIT, not including unpaid interests /// and creditMultiplier changes that could make debt amounts higher than the initial /// borrowed CREDIT amounts. function totalBorrowedCredit() external view returns (uint256) { return CreditToken(credit).targetTotalSupply() - SimplePSM(psm).redeemableCredit(); }
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SimplePSM.sol
function redeemableCredit() public view returns (uint256) { return getMintAmountOut(pegTokenBalance); } function getMintAmountOut(uint256 amountIn) public view returns (uint256) { uint256 creditMultiplier = ProfitManager(profitManager) .creditMultiplier(); return (amountIn * decimalCorrection * 1e18) / creditMultiplier; }
The totalBorrowedCredit
function is reachable from several entry points including borrow()
, unstake()
, updateMintRatio()
, and applyGaugeLoss()
. If a large enough loss occurs, all these functions will revert, effectively bricking the protocol.
The root cause of this issue is the way totalBorrowedCredit
is calculated. It subtracts redeemableCredit
from targetTotalSupply
. However, redeemableCredit
is calculated using the creditMultiplier
, which can decrease in the event of a loss. This is incorrect and was not the intention of the developers, as can be understood from totalBorrowedCredit()
's documentation. If the creditMultiplier
is low enough, redeemableCredit
can exceed targetTotalSupply
, causing the subtraction to underflow and the function to revert.
Consider the following scenario:
targetTotalSupply
of 300 CREDIT and a pegTokenBalance
of 200.creditMultiplier
to decrease to 0.5.redeemableCredit
is 400 (βpegTokenBalance / creditMultiplier
), which is greater than targetTotalSupply
.totalBorrowedCredit
now causes an underflow and reverts.This scenario is illustrated in the testTotalBorrowedCredit_breaks
test below:
function testTotalBorrowedCredit_breaks() public { assertEq(profitManager.totalBorrowedCredit(), 100e18); // psm mint 200 CREDIT pegToken.mint(address(this), 200e6); pegToken.approve(address(psm), 200e6); psm.mint(address(this), 200e6); assertEq(pegToken.balanceOf(address(this)), 0); assertEq(pegToken.balanceOf(address(psm)), 200e6); assertEq(credit.balanceOf(address(this)), 300e18); assertEq(profitManager.totalBorrowedCredit(), 100e18); vm.startPrank(governor); core.grantRole(CoreRoles.GAUGE_PNL_NOTIFIER, address(this)); vm.stopPrank(); // apply a loss // 150 CREDIT of loans completely default (~150 USD loss) profitManager.notifyPnL(address(this), -150e18); assertEq(profitManager.creditMultiplier(), 0.5e18); // 50% discounted // totalBorrowedCredit() reverts since targetTotalSupply() is still 300 // but redeemableCredit() is 400 (= pegTokenBalance / creditMultiplier) vm.expectRevert(); profitManager.totalBorrowedCredit(); }
Foundry
Do not use the value returned by redeemableCredit
to calculate the totalBorrowedCredit
. Instead, use the amount that has been minted through the PSM without scaling.
Under/Overflow
#0 - c4-pre-sort
2023-12-30T14:10:12Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-30T14:10:37Z
0xSorryNotSorry marked the issue as duplicate of #1170
#2 - c4-judge
2024-01-28T23:36:06Z
Trumpero changed the severity to 2 (Med Risk)
#3 - Trumpero
2024-01-29T13:33:37Z
This issue has a valid description and root cause of an underflow vulnerability in the totalBorrowedCredit() function. However, the scenario of the PoC cannot happen in an operational context: for a loss of 150 CREDIT to occur, a loan of 150 CREDIT has to be opened. But if totalSupply is 300 and psm mints are 200, the maximum loan size and loss that can happen is 100. So I believe 75% partial credit is appropriate for this issue.
#4 - c4-judge
2024-01-29T13:33:51Z
Trumpero marked the issue as satisfactory
#5 - c4-judge
2024-01-29T13:33:55Z
Trumpero marked the issue as partial-75
π Selected for report: serial-coder
Also found by: 0xStalin, 0xbepresent, Cosine, DanielArmstrong, EV_om, HighDuty, Soul22, SpicyMeatball, ether_sky, evmboi32, gesha17, kaden, lsaudit, nonseodion, smiling_heretic
42.2419 USDC - $42.24
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L154 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L191-L195
The LendingTermOffboarding
contract has a potential issue that could lead to an inconsistent state in the system. This inconsistency could brick redemptions and disrupt the normal functioning of the lending term offboarding process.
The issue arises when a lending term is offboarded and then immediately re-onboarded without the cleanup()
function being called. This would allow anyone to immediately offboard the term again, leading to an incorrect value in nOffboardingsInProgress
. This would in turn block the unpausing of redemptions in the PSM
as the nOffboardingsInProgress
variable could not be decreased down to 0 again.
Consider the following sequence of events:
LendingTerm
is offboarded with the intention of calling all loans and immediately re-onboarding it (due to e.g. some loans being so old that the interest accrued brings them close to being underwater)LendingTerm
is immediately re-onboarded.cleanup()
function during the time it is being offboarded.canOffboard[term]
is still true
, anyone can call the offboard()
function again._deprecatedGauges
set again and increases nOffboardingsInProgress
to 2.cleanup()
can only be called once as canOffboard[term]
will be false
on subsequent calls, which makes it impossible to unpause redemptions in the PSM
.https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L154 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L191-L195
Manual review
To mitigate this issue, consider adding a check in the proposeOnboard()
function to ensure that a term cannot be re-onboarded if it hasn't been cleaned up. This could be done by checking if LendingTermOffboarding.canOffboard[term]
is false
before allowing the term to be onboarded.
Other
#0 - c4-pre-sort
2024-01-01T11:27:29Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-01T11:27:54Z
0xSorryNotSorry marked the issue as duplicate of #1147
#2 - c4-judge
2024-01-25T18:49:39Z
Trumpero marked the issue as duplicate of #1141
#3 - c4-judge
2024-01-25T18:54:25Z
Trumpero marked the issue as satisfactory
#4 - c4-judge
2024-01-31T13:45:18Z
Trumpero changed the severity to 2 (Med Risk)
π Selected for report: SBSecurity
Also found by: 0x_6a70, 0xanmol, 0xbepresent, 0xfave, Arz, Byteblockers, CaeraDenoir, EV_om, EllipticPoint, Infect3d, JCN, Mike_Bello90, SECURITISE, Soul22, almurhasan, c47ch3m4ll, carrotsmuggler, cccz, critical-or-high, ether_sky, evmboi32, grearlake, kaden, rbserver, smiling_heretic, whitehat-boys, zhaojie
6.8173 USDC - $6.82
The ProfitManager
contract has a potential vulnerability where an attacker can perform a sandwich attack. This vulnerability arises from the way the notifyPnL
function updates the gaugeProfitIndex
for the reporting gauge immediately when a positive PnL is reported.
Entry points that call notifyPnL
with a positive PnL are LendingTerm.repay()
, LendingTerm.PartialRepay()
, and in some cases AuctionHouse.bid()
. Unlike rewards to CREDIT holders, rewards to GUILD holders aren't distributed gradually. This means an attacker can sandwich any of these calls, increasing their weight in this gauge, immediately call ProfitManager.claimGaugeRewards()
or SurplusGuildMinter.getRewards()
afterwards to reap the rewards, and then unstake/decrease their weight again.
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/ProfitManager.sol#L396-L399 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/ProfitManager.sol#L427-L435
An attacker can follow these steps to exploit the vulnerability:
LendingTerm.repay()
, LendingTerm.PartialRepay()
, or AuctionHouse.bid()
during the first phase of an auction.claimGaugeRewards()
or getRewards()
to claim the rewards.This sequence of actions allows the attacker to unfairly claim more rewards than they should be entitled to.
Manual review
To mitigate this vulnerability, consider implementing a mechanism to distribute GUILD rewards gradually, similar to how CREDIT rewards are distributed. This could prevent an attacker from being able to immediately claim rewards after increasing their weight in the gauge. Additionally, consider implementing measures to prevent rapid changes in gauge weight, such as rate limiting or cooldown periods.
Other
#0 - c4-pre-sort
2023-12-29T19:26:37Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-29T19:26:52Z
0xSorryNotSorry marked the issue as duplicate of #877
#2 - c4-pre-sort
2023-12-30T16:06:03Z
0xSorryNotSorry marked the issue as not a duplicate
#3 - c4-pre-sort
2023-12-30T16:06:11Z
0xSorryNotSorry marked the issue as duplicate of #994
#4 - c4-judge
2024-01-25T18:10:21Z
Trumpero changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-01-25T18:16:49Z
Trumpero marked the issue as satisfactory
35.7813 USDC - $35.78
In the ERC20RebaseDistributor
contract, the distribute
function is designed to distribute tokens proportionately to all rebasing accounts. However, there is a potential issue where anyone can dilute rewards by distributing a minimal amount of tokens (1 wei).
The root cause of this issue lies in the distribute
function. When a distribution is made, the endTimestamp
is simply extended up to block.timestamp + DISTRIBUTION_PERIOD
, which means the current distribution is extended. This allows for a scenario where an attacker could repeatedly distribute a minimal amount of tokens (1 wei), diluting the rewards for other users.
Consider the following scenario:
DISTRIBUTION_PERIOD
), with many users set to receive a significant amount of tokens.distribute
function.endTimestamp
is simply extended up to DISTRIBUTION_PERIOD
in each distribute
call, the attacker's distribution dilutes the rewards that were intended for the other users (in this case, halving them in all future blocks).Manual review
A more comprehensive solution could involve interpolating rewards individually for each distribution. This would mean that each distribution, which should be of DISTRIBUTION_PERIOD
duration, would have its rewards calculated separately. This would prevent the dilution of rewards when a new distribution is made before the end of the current distribution period. However, the gas costs imposed by such an implementation could be prohibitively high, making this an unrealistic solution in practice.
Additionally, the distribute
function could be restricted to being only callable by the ProfitManager
. This would not directly fix the issue of reward dilution, but it would prevent a direct attack as per the example scenario. This would add an additional layer of security, as only the ProfitManager
would be able to call the distribute
function and potentially dilute rewards.
Other
#0 - c4-pre-sort
2024-01-03T16:50:24Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-03T16:51:00Z
0xSorryNotSorry marked the issue as duplicate of #1100
#2 - c4-judge
2024-01-29T22:05:47Z
Trumpero marked the issue as satisfactory
π Selected for report: niroh
Also found by: EV_om, EllipticPoint, carrotsmuggler, kaden, rvierdiiev
323.0626 USDC - $323.06
The LendingTerm.debtCeiling()
function, only called from GuildToken._decrementGaugeWeight()
, which is itself internally called when unstaking/decreasing gauge weight, contains a mistake in calculation that will prevent users from unstaking from any term if the creditMinterBuffer
is low. This is because it will always return the creditMinterBuffer
if it is lower than the _hardCap
or _debtCeiling
. The only exception is the case where _issuance >= debtCeilingBefore
, in which case debtCeilingBefore
will be returned, which will then fail the require(issuance <= debtCeilingAfterDecrement)
statement in _decrementGaugeWeight()
unless in case of strict equality.
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/LendingTerm.sol#L290-L291 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/LendingTerm.sol#L302-L303 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/LendingTerm.sol#L309-L311 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/LendingTerm.sol#L316-L317 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/LendingTerm.sol#L323-L330 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/tokens/GuildToken.sol#L226-L230
It seems that this function was written with another purpose in mind (to limit minting) and then repurposed to fulfil another purpose (to determine how much can be unstaked).
This could lead to a situation where, under heavy protocol usage or simply after a large mint, users are unable to unstake or decrease their gauge weight, regardless of the issuance on the term they're supporting.
Consider the following scenario:
creditMinterBuffer
.GuildToken._decrementGaugeWeight()
function is called, which internally calls the debtCeiling()
function.debtCeiling()
function returns the creditMinterBuffer
as it is lower than the _hardCap
or _debtCeiling
.creditMinterBuffer
is lower than the term's issuance.Additionally, note that this also opens up a DoS vector (albeit probably beneficial to the protocol) exploitable by continually minting CREDIT and depleting the minting buffer, which will prevent anybody from unstaking or reducing their weight.
Manual review
The debtCeiling()
function needs to be re-architected to ensure it only prevents unstaking based on a term's issuance.
Other
#0 - c4-pre-sort
2024-01-01T11:29:31Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-01T11:29:47Z
0xSorryNotSorry marked the issue as duplicate of #878
#2 - c4-judge
2024-01-25T18:19:49Z
Trumpero changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-01-25T18:33:02Z
Trumpero marked the issue as grade-b
#4 - c4-judge
2024-01-30T13:32:47Z
This previously downgraded issue has been upgraded by Trumpero
#5 - c4-judge
2024-01-30T17:47:10Z
Trumpero changed the severity to QA (Quality Assurance)
#6 - c4-judge
2024-01-30T19:43:50Z
Trumpero removed the grade
#7 - c4-judge
2024-01-30T19:43:54Z
Trumpero marked the issue as grade-b
#8 - Trumpero
2024-01-31T14:28:08Z
I consider this issue as a dup of https://github.com/code-423n4/2023-12-ethereumcreditguild-findings/issues/868 since it shares the same idea about the problem, where capping debtCeiling with creditMinterBuffer causes the debtCeiling to be lower than it should.
#9 - thebrittfactor
2024-01-31T16:08:33Z
For transparency, the judge has requested for the labels on this submission to be updated.
#10 - c4-judge
2024-01-31T16:19:18Z
Trumpero marked the issue as satisfactory