Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slashed staking positions can still claim rewards #117

Closed
c4-bot-6 opened this issue Dec 16, 2023 · 6 comments
Closed

Slashed staking positions can still claim rewards #117

c4-bot-6 opened this issue Dec 16, 2023 · 6 comments
Labels
bug Something isn't working downgraded by judge Judge downgraded the risk level of this issue duplicate-262 edited-by-warden grade-c QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sufficient quality report This report is of sufficient quality unsatisfactory does not satisfy C4 submission criteria; not eligible for awards

Comments

@c4-bot-6
Copy link
Contributor

c4-bot-6 commented Dec 16, 2023

Lines of code

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

Vulnerability details

Impact

A user can stake his Credit tokens in SGM, and SGM goes ahead and mints Guild tokens and adds weight to a Gauge that the user chooses. If that Gauge gains profit the user who staked his tokens can claim rewards in the Credit token, in case of loss, the staking position is slashed, i.e. deleted, and the user loses all his stakings. Both the slashing and rewards claiming happen in the same function, getRewards. So in theory if a Guage that a user weighted in lost a certain amount then profited some amount, the user should be slashed and he shouldn't get any rewards from that position.

However, if that happens and getRewards isn't called between the loss and the profit, that user can claim undeserved rewards, where he should've lost everything in that staking position.

Even though getRewards can be called permissionless, i.e. anyone or a bot can call it, however, the protocol shouldn't depend on something like "if the function was anonymously called on time", this case should be handled explicitly.

Proof of Concept

The following test shows the current behavior, where a user claims rewards after being supposedly slashed.

function test_ClaimRewardsAfterSlash() public {
    credit.mint(address(this), 10e18);
    credit.approve(address(sgm), 10e18);
    sgm.stake(term, 10e18);

    assertEq(credit.balanceOf(address(this)), 0);

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

    (
        uint256 lastGaugeLoss,
        SurplusGuildMinter.UserStake memory userStake,
        bool slashed
    ) = sgm.getRewards(address(this), term);

    assertEq(lastGaugeLoss, 0);
    assertEq(userStake.credit, 10e18);
    assertEq(userStake.guild, 20e18);
    assertEq(slashed, false);

    vm.prank(governor);
    profitManager.setProfitSharingConfig(0.5e18, 0, 0.5e18, 0, address(0));
    profitManager.notifyPnL(term, -1);

    // `getRewards` or any other function that calls `getRewards` didn't get the chance to be called

    assertEq(credit.balanceOf(address(this)), 0);

    credit.mint(address(profitManager), 35e18);
    profitManager.notifyPnL(term, 35e18);

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

    (lastGaugeLoss, userStake, slashed) = sgm.getRewards(
        address(this),
        term
    );

    assertEq(lastGaugeLoss, block.timestamp - 13);
    assertEq(userStake.credit, 0);
    assertEq(userStake.guild, 0);
    assertEq(slashed, true);

    assertEq(credit.balanceOf(address(this)), 5e18);
}

The following test shows the normal scenario where getRewards is called between loss and profit, the user lost everything and didn't get any rewards.

function test_ClaimRewardsAfterSlash_normalScenario() public {
    credit.mint(address(this), 10e18);
    credit.approve(address(sgm), 10e18);
    sgm.stake(term, 10e18);

    assertEq(credit.balanceOf(address(this)), 0);

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

    (
        uint256 lastGaugeLoss,
        SurplusGuildMinter.UserStake memory userStake,
        bool slashed
    ) = sgm.getRewards(address(this), term);

    assertEq(lastGaugeLoss, 0);
    assertEq(userStake.credit, 10e18);
    assertEq(userStake.guild, 20e18);
    assertEq(slashed, false);

    vm.prank(governor);
    profitManager.setProfitSharingConfig(0.5e18, 0, 0.5e18, 0, address(0));
    profitManager.notifyPnL(term, -1);

    (lastGaugeLoss, userStake, slashed) = sgm.getRewards(
        address(this),
        term
    );

    assertEq(credit.balanceOf(address(this)), 0);

    credit.mint(address(profitManager), 35e18);
    profitManager.notifyPnL(term, 35e18);

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

    (lastGaugeLoss, userStake, slashed) = sgm.getRewards(
        address(this),
        term
    );

    assertEq(lastGaugeLoss, block.timestamp - 13);
    assertEq(userStake.credit, 0);
    assertEq(userStake.guild, 0);
    assertEq(slashed, true);

    assertEq(credit.balanceOf(address(this)), 0);
}

Tools Used

Manual review + vscode

Recommended Mitigation Steps

The protocol should check if a user is slashed before transferring the rewards. However, this introduces another issue, that is the reverse, if a Gauge profited then lost, the user should still be able to claim the promised rewards, ...

Assessed type

Invalid Validation

@c4-bot-6 c4-bot-6 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Dec 16, 2023
c4-bot-1 added a commit that referenced this issue Dec 16, 2023
@c4-pre-sort c4-pre-sort added the sufficient quality report This report is of sufficient quality label Jan 3, 2024
@c4-pre-sort
Copy link

0xSorryNotSorry marked the issue as sufficient quality report

@c4-pre-sort
Copy link

0xSorryNotSorry marked the issue as duplicate of #956

@c4-judge
Copy link
Contributor

Trumpero marked the issue as duplicate of #262

@c4-judge
Copy link
Contributor

Trumpero changed the severity to QA (Quality Assurance)

@c4-judge c4-judge added downgraded by judge Judge downgraded the risk level of this issue QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed 3 (High Risk) Assets can be stolen/lost/compromised directly labels Jan 28, 2024
@c4-judge
Copy link
Contributor

Trumpero marked the issue as grade-b

@c4-judge c4-judge added grade-b grade-c unsatisfactory does not satisfy C4 submission criteria; not eligible for awards and removed grade-b labels Jan 28, 2024
@c4-judge
Copy link
Contributor

Trumpero marked the issue as grade-c

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working downgraded by judge Judge downgraded the risk level of this issue duplicate-262 edited-by-warden grade-c QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sufficient quality report This report is of sufficient quality unsatisfactory does not satisfy C4 submission criteria; not eligible for awards
Projects
None yet
Development

No branches or pull requests

4 participants