xanoutas/solidity-security-findings huggingface dataset:
Signatures can be replayed with castVoteWithReasonAndParamsBySig() to cast more votes than the user intended
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/fd81a96f01cc42ef1c9a5399364968d0e07e9e90/contracts/governance/Governor.sol#L524-L539
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/GuildGovernor.sol#L4


# Vulnerability details

## Impact
In the GuildGovernor contract, `castVoteWithReasonAndParamsBySig()` function allows the user to vote with the signature, which is in Openzeppelin's Governor:

<https://github.com/OpenZeppelin/openzeppelin-contracts/blob/fd81a96f01cc42ef1c9a5399364968d0e07e9e90/contracts/governance/Governor.sol#L524-L539>

Also, take a look at this report. <https://github.com/code-423n4/2023-08-arbitrum-findings/issues/252>


## Proof of Concept

-  Bob has 1000 votes, and he wants to split his votes between proposals A and B.
    -  He signs one signature to give 500 votes to proposal A.
    -  He signs a second signature to allocate 500 votes to proposal B.
- castVoteWithReasonAndParamsBySig() is called to submit Bob's first signature:
    - This gives proposal A 500 votes.
- After the transaction is executed, Alice sees Bob's signature in the transaction.
- As Alice wants proposal A to be elected, she calls castVoteWithReasonAndParamsBySig() with Bob's first signature again:
    - Due to a lack of a nonce, the transaction is executed successfully, giving proposal A another 500 votes.
- Now, when castVoteWithReasonAndParamsBySig() is called with Bob's second signature, it reverts as all his 1000 votes are already allocated to proposal A.
In the scenario above, Alice has managed to allocate all of Bob's votes to proposal A against his will.

As seen from above, the signature provided does not include a nonce.

Due to the lack of a nonce, `castVoteWithReasonAndParamsBySig()` can be called multiple times with the same signature.

Therefore, if a user provides a signature to use a portion of his votes, an attacker can repeatedly call `castVoteWithReasonAndParamsBySig()` with the same signature to use up more votes than the user originally intended.

## Tools Used
manual review 

## Recommended Mitigation Steps

Consider adding a way to protect against signature replay in the GuildGovernor contracts. Make sure to use the latest version of OpenZeppelin; they added the nonce feature.

<https://github.com/OpenZeppelin/openzeppelin-contracts/blob/a72c9561b9c200bac87f14ffd43a8c719fd6fa5a/contracts/governance/Governor.sol#L577C1-L607>

```javascript
        bool valid = SignatureChecker.isValidSignatureNow(
            voter,
            _hashTypedDataV4(
                keccak256(
                    abi.encode(
                        EXTENDED_BALLOT_TYPEHASH,
                        proposalId,
                        support,
                        voter,
@>                      _useNonce(voter),
                        keccak256(bytes(reason)),
                        keccak256(params)
                    )
                )
A Malicious Guild holder can propose the deployment  of a malicious LoanTerm
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/LendingTerm.sol#L354-L362


# Vulnerability details

## Impact
A malicious guild holder can propose a LoanTerm that can allow withdrawal of maxBorrow(hardCap) with one wei of the collateraToken token, quickly take the loan with one wei as collateralAmount after deployment and default, then use the credit token obtained from this malicious act to stake on profitable gauges and earn rewards.

## Proof of Concept
1. The attacker has the required amount of Guild to make a LoanTerm proposal;
2. Attacker creates  loan term with USDC as collateral token, and set maxDebtPerCollateralToken to be 1e42
3. Attacker proposeOnboard,  other malicious Guild holders agrees to support the proposal.
4. If the proposal passes without being vetioed, the attacker quickly calls borrow on the loanTerm with one wei as collateral.
5. maxBorrow is checked to be; 
``` solidity
uint256 maxBorrow = (collateralAmount *
            params.maxDebtPerCollateralToken) / creditMultiplier;
        require(
            borrowAmount <= maxBorrow,
            "LendingTerm: not enough collateral"
        );

```
```1 * 1e42 / 1e18 = 1e24```, with this the attacker can get 1,000,000 gUSDC with just one wei of collateral and can make more profit by staking on profitable guages.

## Tools Used
Manual review. 

## Recommended Mitigation Steps
Value of collateral must be compared to value of credit Token being taken, and there must be a threshold to compare it with. 


## Assessed type

Math
User can manipulate his own userGaugeWeight to steal others' rewards from ProfitManager
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L414


# Vulnerability details

## Impact
In ProfitManager.claimGaugeRewards function, user gets rewards proportional to the difference in gaugeProfitIndex and userGaugeProfitIndex and also proportional to the existing userGaugeWeight which is the GUILD weight user has posted in a gauge. But this userGaugeWeight is the current weight which can be manipulated in real-time very easily.

Suppose a user has contributed 1e18 GUILD to a gauge and never claimed any rewards. So his userGaugeProfitIndex will be 0. 

Now he sees that much of the profit has accumulated => gaugeProfitIndex = 10e18. Now he increases his userGaugeWeight for this particular gauge using incrementGauge. This increases the userGaugeWeight without claiming the pending rewards for the already existing userGaugeWeight.

This way now his userGaugeWeight  = 100e18. Now when he calls ProfitManager.claimGaugeRewards, he gets creditEarned = 100e18 * (gaugeProfitIndex - userGaugeProfitIndex) = 900e18. But he only deserved 9e18 credit rewards based on his previous userGaugeWeight that was posted for that much time duration. This amount is huge and might even be greater than the actual credit rewards earned by the whole gauge as a share of the guildSplit  => which means credit balance accrued in ProfitManager from other gauges' profits as well as the surplusBuffer and termSurplusBuffers may get used up in this rewards transfer.

The user can do this repeatedly for all gauges whenever he wants. High risk because it steals other users' rewards for all gauges + also depletes the insurance fund(surplusBuffer) which was meant to cover bad debt, thus causing more harm to lenders. 

## Proof of Concept
ERC20Gauges.incrementGauge function does not accrue pending rewards so userGaugeWeight can be freely manipulated : https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/tokens/ERC20Gauges.sol#L219

credit rewards for user are calculated using the current userGaugeWeight which has been manipulated.
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L414

## Tools Used
Manual review

## Recommended Mitigation Steps
Add a call to claimGaugeRewards in the incrementGauge function so that pending rewards are claimed first and userGaugeProfitIndex is updated before adding new weight to the userGaugeWeight, just like in SurplusGuildMinter.


## Assessed type

Context
Users loose all GUILD tokens voted for a gauge by forced loss
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/LendingTerm.sol#L634-L675
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L305
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/tokens/GuildToken.sol#L123-L129
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/tokens/GuildToken.sol#L133-L161


# Vulnerability details

When a gauge experiences a loss, usually either by calling loans of offboarded lending terms or overdue partial payment,  `ProfitManager.notifyPnL()` is called, which internally notify gauge loss to GUILD token:

```javascript
    function notifyPnL(
        address gauge,
        int256 amount
    ) external onlyCoreRole(CoreRoles.GAUGE_PNL_NOTIFIER) {
        uint256 _surplusBuffer = surplusBuffer;
        uint256 _termSurplusBuffer = termSurplusBuffer[gauge];
        address _credit = credit;

        // handling loss
        if (amount < 0) {
            uint256 loss = uint256(-amount);

            // save gauge loss
            GuildToken(guild).notifyGaugeLoss(gauge);
			// [...]
```

Then, in order to perform any action on the GUILD tokens, user has to apply the loss to tgheir account. However, no matter how big the loss is, ALL his stake is burned from them in the following code: 
```javascript
    function applyGaugeLoss(address gauge, address who) external
        // [...]
	
        // read user weight allocated to the lossy gauge
        uint256 _userGaugeWeight = getUserGaugeWeight[who][gauge];

        // remove gauge weight allocation
        lastGaugeLossApplied[gauge][who] = block.timestamp;
        _decrementGaugeWeight(who, gauge, _userGaugeWeight); // @issue burning all user assets no matter the loss
        if (!_deprecatedGauges.contains(gauge)) {
            totalTypeWeight[gaugeType[gauge]] -= _userGaugeWeight;
            totalWeight -= _userGaugeWeight;
        }

        // apply loss
        _burn(who, uint256(_userGaugeWeight)); // @audit user is burned whole stake, no matter how big is the loss
```

The worst part is that the loss can be forced by creating a small loan and not performing partial repay and then calling a loan. Then, an auction is started, and when it's close to being profitable, a malicious actor can block stuff until protocol loss is incurred. It actually may even happen naturally, without anyone's action due to delays in processing, L2 sequencer being down etc. Actually, the biggest risk of this happening is on L2s which are less stable than Ethereum and block stuffing attack is extremely cheap.

## Impact
Users loose all GUILD tokens staked on a gauge by forced loss. Given that it's hard to get GUILD tokens and they are untransferrable by default, can even impair voting completely, in extreme case not all
New user is able to gain ALL rewards since protocol launch
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L409-L436


# Vulnerability details

Gauge rewards in ECG are similar to Compound - there is a global ever-increasing profit index, and one profit index per user, synchronized by applying the delta between `gaugeProfitIndex` and `userGaugeProfitIndex`. 

When user profit index is not initialized, it's set to 1e18. This is an issue when the ProfitManager operates for some time and the gaugeProfitIndex grew to a new value. Then we have following situation:

`gaugeProfitIndex = 1.2e18`
User just joined, userGaugeProfitIndex  is set to `1e18`. Hence He's elligible to all rewards since gauge started operating.

The problem is even worse if the Guild token transfer are enabled. Then a malicious actor can use sybil accounts and claim all the rewards of other users to themselves.

## Impact
All rewards are stolen by sybil accounts, able to claim all rewards since the protocol launch.

## Proof of Concept
1. Wait for `gaugeProfitIndex` to grow.
2. Call `GuildToken.incrementGauge()`. From a new account. This will internally call `ProfitManager.claimGaugeRewards()`.
3. Because the user didn't claimed their profit before, their profit will be initialzed to 1e18, allowing them to claim all rewards since beginning.
4. If Guild token transfers are enabled, the user can get the tokens back, transfer the amount to their account and repeat the attack.

## Tools Used

Manual analysis

## Recommended Mitigation Steps
In case that the user doesn't have index assigned, assign it to current gauge profit index and return from the function early:
```diff
    function claimGaugeRewards(
        address user,
        address gauge
    ) public returns (uint256 creditEarned) {
        uint256 _userGaugeWeight = uint256(
            GuildToken(guild).getUserGaugeWeight(user, gauge)
        );
        if (_userGaugeWeight == 0) {
            return 0;
        }
        uint256 _gaugeProfitIndex = gaugeProfitIndex[gauge];
        uint256 _userGaugeProfitIndex = userGaugeProfitIndex[user][gauge];
        if (_gaugeProfitIndex == 0) {
            _gaugeProfitIndex = 1e18;
        }
        if (_userGaugeProfitIndex == 0) {
-            _userGaugeProfitIndex = 1e18;
+            userGaugeProfitIndex[user][gauge] = _gaugeProfitIndex;
+            return 0;
```


## Assessed type

Math
All rewards are lost with moderate amount of GUILD tokens voted for a gauge
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L292-L405


# Vulnerability details

When the gauge notifies about profit, it calls `ProfitManager.notifyPnL()`:
```javascript
function notifyPnL(
        address gauge,
        int256 amount // @audit overflow somewhere?
    ) external onlyCoreRole(CoreRoles.GAUGE_PNL_NOTIFIER) {
// [...]
            // distribute to the guild
            if (amountForGuild != 0) {
                // update the gauge profit index
                // if the gauge has 0 weight, does not update the profit index, this is unnecessary
                // because the profit index is used to reattribute profit to users voting for the gauge,
                // and if the weigth is 0, there are no users voting for the gauge.
                uint256 _gaugeWeight = uint256(
                    GuildToken(guild).getGaugeWeight(gauge)
                );
                if (_gaugeWeight != 0) {
                    uint256 _gaugeProfitIndex = gaugeProfitIndex[gauge];
                    if (_gaugeProfitIndex == 0) {
                        _gaugeProfitIndex = 1e18;
                    }
                    gaugeProfitIndex[gauge] =
                        _gaugeProfitIndex +
                        (amountForGuild * 1e18) /
                        _gaugeWeight;
                }
            }
        }
```

The amount of rewards to distribute are gathered in `gaugeProfitIndex` and are added with amount of rewards scaled down by gauge weigth. There is a problem with this calculation however. 
The problem is that the gauge weigth is an absolute value, not a percentage of total weigth. Hence, `_gaugeWeight` in the code above with 1000 GUILD tokens voted on the gauge with be 1000e18. Even with such a small amount, it will probably be bigger than many profits. Given that users are economically encouraged to stake their tokens, it wil grow to a number that would always round down to 0, leaving users with no rewards.

## Impact
Rewards loss for GUILD holders. Given that the holders can lost their all stake when a gauge experiences loss and effectively have no benefit from staking it, it undermines meaning of staking tokens.


## Tools Used

Manual analysis

## Recommended Mitigation Steps

```diff
                    gaugeProfitIndex[gauge] =
                        _gaugeProfitIndex +
-                        (amountForGuild * 1e18) /
-                        _gaugeWeight;
+                        (amountForGuild * 1e18 * _gaugeWeight) /
+                        GuildToken(guild).totalWeigth();
                }
```


## Assessed type

Math
Stakers can prevent losses by frontrunning bad debts.
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L309
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/SurplusGuildMinter.sol#L158


# Vulnerability details

## Impact
The `SurplusGuildMinter` contract allows users to provide CREDIT tokens as first-loss capital to the surplus buffer of selected terms. In return, they can participate in the gauge voting system at a lower capital cost and without being affected by fluctuations in the price of GUILD token.

If a bad debt occurs, there will be a loss and all the credit tokens allocated to that term/guage will become zero. 
Stakers can prevent losses by unstaking credit tokens right before the liquidation transaction.

## Proof of Concept
Copy and paste the following code into the `SurplusGuildMinterUnitTest.t.sol` file.
```solidity
    function testUnstakeWithFrontrunningAndLoss() public {
        // setup
        credit.mint(address(this), 150e18);
        credit.approve(address(sgm), 150e18);
        sgm.stake(term, 150e18);
        assertEq(credit.balanceOf(address(this)), 0);
        assertEq(profitManager.surplusBuffer(), 0);
        assertEq(profitManager.termSurplusBuffer(term), 150e18);
        assertEq(guild.balanceOf(address(sgm)), 300e18);
        assertEq(guild.getGaugeWeight(term), 350e18); 
        assertEq(sgm.getUserStake(address(this), term).credit, 150e18);

        address bobby = address(0xb0b);
        credit.mint(bobby, 150e18);
        vm.startPrank(bobby);
        credit.approve(address(sgm), 150e18);
        sgm.stake(term, 150e18);
        vm.stopPrank();

        // the guild token earn interests
        vm.prank(governor);
        profitManager.setProfitSharingConfig(
            0.5e18, // surplusBufferSplit
            0, // creditSplit
            0.5e18, // guildSplit
            0, // otherSplit
            address(0) // otherRecipient
        );
        credit.mint(address(profitManager), 35e18);
        profitManager.notifyPnL(term, 35e18);
        assertEq(profitManager.surplusBuffer(), 17.5e18);//note 35e18/2
        assertEq(profitManager.termSurplusBuffer(term), 300e18);
        (,, uint256 rewardsThis) = profitManager.getPendingRewards(address(this));
        (,, uint256 rewardsSgm) = profitManager.getPendingRewards(address(sgm));
        // assertEq(rewardsThis, 2.5e18); 
        // assertEq(rewardsSgm, 15e18);

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

        //note frontrunning 
        vm.prank(bobby);
        sgm.unstake(term, 150e18);
        assertEq(credit.balanceOf(bobby), 150e18 + rewardsSgm / 2); // Bobby receives both his initial deposits and rewards.

        // loss in gauge
        profitManager.notifyPnL(term, -27.5e18);
        assertEq(profitManager.surplusBuffer(), 17.5e18 + 150e18 - 27.5e18); // 140e18
        assertEq(p
User can auction on his own loan, receive all the collateral & cause a total loss to the protocol by paying nothing
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/LendingTerm.sol#L773-L780


# Vulnerability details

In case a gauge is depreciated or a user misses a partial repayment on his loan, anyone can call the loan to put it up for auction. Even the borrowers can themselves can participate in the auction & bid on their own loans.

However it is possible for the borrower to bid on its own loan retrieving all the collateral without paying anything to the protocol.

*Even though the vulnerability can be mitigated by a simple check, its absence benefits the attacker 100% & causes 100% loss to the protocol. That's why labelling it as High severity*

## Proof of Concept

### Attack Scenario
- Bob does not pay the partial repayment on his loan.
- Bob executes `call()` to open an auction for his loan.
- Bob **immediately** calls `bid()` with a **different address** that contains 0 credit token as soon as the auction starts.
- The `LendingTerm(_lendingTerm).onBid()` is called with `creditAsked = auctions[loanId].callDebt` & `collateralReceived = 0`.
- Inside `onBid()` all the checks are passed & the debt is calculated.
- Now the amount from the bidder (Bob) is transferred here.

```solidity
File: LendingTerm.sol

725:   function onBid(
       ...
773:   // pull credit from bidder
        if (creditFromBidder != 0) {
            CreditToken(refs.creditToken).transferFrom(
                bidder,
                address(this),
                creditFromBidder
            );
        }
```
- Inside `transferFrom()` in `CreditToken` contract, the following two functions are called. 

```solidity
File: CreditToken.sol

134:     _decrementVotesUntilFree(from, amount); // from ERC20MultiVotes
135:     return ERC20RebaseDistributor.transferFrom(from, to, amount);
```

- `_decrementVotesUntilFree()` does not cause any changes in the protocol.
- `ERC20RebaseDistributor.transferFrom()` would simply return `false` in this case as can be seen below.

```solidity
File: ERC20RebaseDistributor.sol

646:    function transferFrom(
        ......
678     bool success = ERC20.transferFrom(from, to, amount);
        ......
735:    return success;
```
- Now inside `onBid()`, the remaining functions are executed. The principal credit token is burned. The profits are transferred to the ProfitManager from the lending term contract even though nothing is received from the bidder followed by decrease of issuance.
- All the collateral has been sent to Bob's original account from which the loan was issued & the protocol receives no funds.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Make sure to revert in case transfer/transferFrom function fails instead of just returning false.


## Assessed type

Token-Transfer
Borrowers can lose their collateral if a proposal for offboarding is approved in a short time
Audit: 2023-12-ethereumcreditguild-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/governance/LendingTermOffboarding.sol#L159


# Vulnerability details

## Impact
For the liquidation of a borrower's position, either the lending term should be deprecated or a minimum partial repayment time should have passed. If the lending term is active and a proposal for offboarding the term is created, is it possible the proposal to be approved in a short time, leading to the deprecation of the lending term. This could lead to the borrowers losing their collaterals since anyone can liquidate their position.

Anyone can create a proposal for offboarding a lending term by simply calling the `proposeOffboard` function in the `LendingTermOffboarding` contract. The reason for this may be a misconfiguration or the lending term having bad debt. Additionally, anyone holding the `GUILD` token can vote through calling the `supportOffboard` function. The maximum time for approving a proposal is approximately 7 days (46523 blocks). To approve a proposal, a quorum must be reached. 

It is possible for a quorum to be reached in a short time, around 1-2 hours, leading to the offboarding of the lending term, resulting in its deprecation and new loans can not be created. This opens up potential problems for active borrowers of this lending term because their loans can be called and sent to auction. The active borrowers may not be able to react in such a short time and they will lose their collateral.

## Tools Used
Mannual Review

## Recommended Mitigation Steps
When a LendingTerm is offboarded, give borrowers at least 1 day to repay their loans.


## Assessed type

Invalid Validation