Ethereum Credit Guild - alexzoid's results

A trust minimized pooled lending protocol.

General Information

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

Ethereum Credit Guild

Findings Distribution

Researcher Performance

Rank: 86/127

Findings: 2

Award: $62.65

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

3.0466 USDC - $3.05

Labels

bug
3 (High Risk)
high quality report
satisfactory
edited-by-warden
duplicate-473

External Links

Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SurplusGuildMinter.sol#L229

Vulnerability details

Impact

Due to the uninitialized userStake.lastGaugeLoss in the SurplusGuildMinter.getRewards() function, a new user's stake is consistently and erroneously slashed. This critical flaw not only imposes unjustified financial losses on users but also undermines the integrity of the staking mechanism.

Proof of Concept

  1. Setting up the Term: Initialize a term with a non-zero lastGaugeLoss in the past and simulate the passage of time.
  2. Initiating New User Stake: Create a new user stake and validate the stake's initialization and credit balance.
  3. Observing the Erroneous Slashing: Advance the blockchain state again and observe the unwarranted slashing with getRewards().

Add the function below into test/unit/loan/SurplusGuildMinter.t.sol.

function testAuditUserAlwaysSlashed() public {
    
    //
    // 1. Setting up the Term (with non-zero `lastGaugeLoss` somewhere in the past)
    // 

    // simulate a past loss to test its impact on new stakers  
    testUnstakeWithLoss();

    // next block
    vm.warp(block.timestamp + 13);
    vm.roll(block.number + 1);

    // there was a loss somewhere in the past
    uint256 lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term);
    assertGt(lastGaugeLoss, 0); 
    assertGt(block.timestamp, lastGaugeLoss);

    //
    // 2. Initiating New User Stake 
    //

    // mint credit for a user
    address newStaker = vm.addr(1);
    credit.mint(newStaker, 100e18);

    // user don't have an active stake yet
    assertEq(sgm.getUserStake(newStaker, term).stakeTime, 0);

    // set up a new staker and confirm initial conditions
    vm.startPrank(newStaker);
    credit.approve(address(sgm), 100e18);
    sgm.stake(term, 100e18);
    assertEq(credit.balanceOf(newStaker), 0); 
    assertEq(sgm.getUserStake(newStaker, term).credit, 100e18);
    vm.stopPrank();

    // new stake `lastGaugeLoss` was initialized with term's `lastGaugeLoss` correctly
    lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term);
    assertEq(sgm.getUserStake(newStaker, term).lastGaugeLoss, lastGaugeLoss);

    // next block
    vm.warp(block.timestamp + 13);
    vm.roll(block.number + 1);

    //
    // 3. Observing the Erroneous Slashing
    //

    (, , bool slashed) = sgm.getRewards(newStaker, term);
    assertTrue(slashed); 
}

Run test with forge test -vv --match-test testAuditUserAlwaysSlashed. Output example:

[â †] Compiling...
No files changed, compilation skipped

Running 1 test for test/unit/loan/SurplusGuildMinter.t.sol:SurplusGuildMinterUnitTest
[PASS] testAuditUserAlwaysSlashed() (gas: 1011790)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.07ms
 
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Tools Used

VSCode, Foundry

It is recommended to initialize the userStake variable before its use in the getRewards() function.

/// @notice get rewards from a staking position without unstaking.
/// This can be used to slash users that have an outstanding unapplied loss.
function getRewards(
    address user,
    address term
)
    public
    returns (
        uint256 lastGaugeLoss, // GuildToken.lastGaugeLoss(term)
        UserStake memory userStake, // stake state after execution of getRewards()
        bool slashed // true if the user has been slashed
    )
{
    userStake = _stakes[user][term];

    bool updateState;
    lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term);
    if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) {
        slashed = true;
    }

    // if the user is not staking, do nothing
    if (userStake.stakeTime == 0)
        return (lastGaugeLoss, userStake, slashed);

    // compute CREDIT rewards
    ProfitManager(profitManager).claimRewards(address(this)); // this will update profit indexes
    uint256 _profitIndex = ProfitManager(profitManager)
        .userGaugeProfitIndex(address(this), term);
    uint256 _userProfitIndex = uint256(userStake.profitIndex);

Assessed type

Other

#0 - c4-pre-sort

2023-12-29T14:50:06Z

0xSorryNotSorry marked the issue as high quality report

#1 - c4-pre-sort

2023-12-29T14:50:26Z

0xSorryNotSorry marked the issue as duplicate of #1164

#2 - c4-judge

2024-01-28T20:20:07Z

Trumpero marked the issue as satisfactory

Findings Information

🌟 Selected for report: SBSecurity

Also found by: 0xanmol, 0xpiken, Giorgio, NentoR, TheSchnilch, alexzoid, asui, btk, ether_sky, grearlake, kaden, santipu_, sl1

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
duplicate-1001

Awards

59.6005 USDC - $59.60

External Links

Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/tokens/GuildToken.sol#L197-L200 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SurplusGuildMinter.sol#L130-L136

Vulnerability details

Impact

The setProfitManager function in GuildToken allows for the changing of the ProfitManager address. However, this change is not reflected in other components like LendingTerm, SimplePSM, and SurplusGuildMinter, which still refer to the old address. This inconsistency can lead to a critical system failure. This discrepancy can result in failed transactions or incorrect reward distributions, leading to system instability and potential financial risks.

Proof of Concept

  1. GuildToken.setProfitManager() is executed with newProfitManager.
  2. SurplusGuildMinter.stake() makes a call to oldProfitManager.donateToTermSurplusBuffer().
  3. Subsequently, there is a call to GuildToken.incrementGauge(), which incorrectly calls newProfitManager.claimGaugeRewards().
  4. There is no functionality to update the ProfitManager address in SurplusGuildMinter as it is in GuildToken.

Tools Used

VSCode

Ensure that the method of updating the ProfitManager address is consistent across all components (GuildToken, LendingTerm, SimplePSM, SurplusGuildMinter). This can be achieved by implementing a similar update mechanism in all components or by restricting the ability to change the ProfitManager in GuildToken to align with other components.

Assessed type

Other

#0 - c4-pre-sort

2024-01-03T21:13:47Z

0xSorryNotSorry marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-03T21:14:06Z

0xSorryNotSorry marked the issue as duplicate of #1001

#2 - c4-judge

2024-01-29T21:37:49Z

Trumpero marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter