KUMA Protocol - Versus contest - 0x52's results

Bringing yield from Real World Assets to DeFi.

General Information

Platform: Code4rena

Start Date: 17/02/2023

Pot Size: $38,600 USDC

Total HM: 5

Participants: 5

Period: 5 days

Judge: GalloDaSballo

Total Solo HM: 2

Id: 217

League: ETH

KUMA Protocol

Findings Distribution

Researcher Performance

Rank: 4/5

Findings: 3

Award: $0.00

QA:
grade-b

🌟 Selected for report: 2

πŸš€ Solo Findings: 2

Findings Information

🌟 Selected for report: hihen

Also found by: 0x52, 0xsomeone, AkshaySrivastav, bin2chen

Labels

2 (Med Risk)
satisfactory
duplicate-13

Awards

Data not available

External Links

Judge has assessed an item in Issue #18 as 2 risk. The relevant finding follows:

[L-01] changePayees can result in broken share count Description for (uint256 i; i < newPayees.length; i++) { if (newPayees[i] == address(0)) { revert Errors.CANNOT_SET_TO_ADDRESS_ZERO(); } if (newShares[i] == 0) { revert Errors.SHARE_CANNOT_BE_ZERO(); }

address payee = newPayees[i]; _payees.add(payee); _shares[payee] = newShares[i]; _totalShares += newShares[i]; emit PayeeAdded(payee, newShares[i]); }

When adding the newPayees to the payees set the contract fails to validate that each address in newPayees is unique. The results is that _totalShares will be incorrect if there is a repeat in newPayees.

Example: Assume newPayees = [Bob,Bob] and newShares = [5,5]. Since _payees.add(payee) is a non-reverting add, if the address already exists in the set it won't revert. The end result is that _totalShares == 10 but _shares[Bob] == 5. This mismatch causes incorrect distribution of rewards.

#0 - c4-judge

2023-03-01T11:03:24Z

GalloDaSballo marked the issue as duplicate of #13

#1 - c4-judge

2023-03-01T16:44:43Z

GalloDaSballo marked the issue as satisfactory

Findings Information

🌟 Selected for report: 0x52

Labels

bug
2 (Med Risk)
disagree with severity
selected for report
M-03

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-02-kuma/blob/3f3d2269fcb3437a9f00ffdd67b5029487435b95/src/kuma-protocol/MCAGRateFeed.sol#L75-L97

Vulnerability details

Impact

MCAGRateFeed#getRate may return stale data

Proof of Concept

(, int256 answer,,,) = oracle.latestRoundData();

Classic C4 issue. getRate only uses answer but never checks the freshness of the data, which can lead to stale bond pricing data. Stale pricing data can lead to bonds being bought and sold on KUMASwap that otherwise should not be available. This would harm KIBToken holders as KUMASwap may accept bond with too low of a coupon and reduce rewards.

Tools Used

Manual Review

Validate that updatedAt has been updated recently enough:

- (, int256 answer,,,) = oracle.latestRoundData(); + (, int256 answer,,updatedAt,) = oracle.latestRoundData(); + if (updatedAt < block.timestamp - MAX_DELAY) { + revert(); + } if (answer < 0) { return _MIN_RATE_COUPON; }

#0 - GalloDaSballo

2023-02-26T19:57:29Z

The Warden has shown how, the system will not check for a stale price

In times of high network congestion, liquidations or if the feed has any downtime, the protocol may end-up using a stale price which can leak value.

For these reasons, I agree with Medium Severity

#1 - c4-judge

2023-02-26T19:57:33Z

GalloDaSballo marked the issue as selected for report

#2 - c4-sponsor

2023-02-28T02:58:36Z

m19 marked the issue as disagree with severity

#3 - m19

2023-02-28T03:01:06Z

This issue is tricky: central bank rate updates are not expected more than a handful of times a year, at most. We failed to document this properly and the lack of a fresh updatedAt check was on purpose.

If we were only transmitting rates when an actual rate change happens, it's impossible to determine how "fresh" the data should be, a central bank rate change could happen every 3 months or twice a year or any timeframe really. So should the staleness check be 90 days, 180 days etc?

We agree that the oracle transmitter could just repeat the current rate on a daily or weekly basis instead making a stale data check useful again.

Because central bank rates are not volatile we still disagree with the severity in this case.

#4 - akshaysrivastav

2023-03-03T05:41:39Z

Did the discussion conclude on this?

Also do the sponsors themselves manage the MCAG feeds? If yes, then this issue won't be as severe as exluding the updatedAt timestamp for an external chainlink price feed. Highly unlikely that the sponsors will stale the feed but keep the rest of the protocol active.

@GalloDaSballo @m19

#5 - GalloDaSballo

2023-03-03T10:29:13Z

I empathize with the Sponsor's answer and agree that in a way the maximum delay for the check is hard to determine, however, it's important that we acknowledge that the smart contract cannot "defend itself" unless we offer some sort of constraint that avoids it leaking value incorrectly.

Will consult with a colleague although I believe that the finding is consistent with the definition of Medium Severity

#6 - GalloDaSballo

2023-03-03T19:11:45Z

In contrast to the front-run, which can be viewed as a nuance / technicality;

The lack of a check would allow the bond to be sold for a time that is not intended, this could also have KumaSwap accept bonds for which the yield is no longer competitive

I believe the observation from the sponsor to be valid the changes may be few and far between, however, because the contract relies on the oracle for it's decision making, and no check is there to ensure that the decision "makes sense";

Given the possibility of:

  • Unintended behaviour (selling of bond when that should not be possible)
  • Loss of yield as a consequence

I believe Medium Severity to be the more appropriate of the options

#7 - GalloDaSballo

2023-03-06T19:28:33Z

The warden has added a comment in terms of suggested mitigation:

We agree that the oracle transmitter could just repeat the current rate on a daily or weekly basis instead making a stale data check useful again.

To quote the sponsor I believe this would be the best course of action. Even thought the rate does not change frequently, its valuable to have a heartbeat to confirm that all systems are working as intended. Better to experience potential downtime for buys/sells than to have systems unknowingly down during an important rate change.

Findings Information

🌟 Selected for report: 0x52

Labels

bug
2 (Med Risk)
selected for report
sponsor confirmed
M-04

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-02-kuma/blob/3f3d2269fcb3437a9f00ffdd67b5029487435b95/src/kuma-protocol/KUMASwap.sol#L115-L171

Vulnerability details

Impact

Selling bonds with coupons that are already accounted will fail unexpectedly

Proof of Concept

https://github.com/code-423n4/2023-02-kuma/blob/3f3d2269fcb3437a9f00ffdd67b5029487435b95/src/kuma-protocol/KUMASwap.sol#L116-L118

if (_coupons.length() == _maxCoupons) { revert Errors.MAX_COUPONS_REACHED(); }

The above lines will cause ALL bonds sales to revert when _coupons.length has reached _maxCoupons. Since bonds may share the same coupon, the swap should continue to accept bonds with a coupon that already exist in the _coupons set.

Tools Used

Manual Review

sellBond should only revert if the max length has been reached and bond.coupon doesn't already exist:

- if (_coupons.length() == _maxCoupons) { + if (_coupons.length() == _maxCoupons && !_coupons.contains(bond.coupon)) { revert Errors.MAX_COUPONS_REACHED(); }

#0 - c4-sponsor

2023-02-28T02:53:13Z

m19 marked the issue as sponsor confirmed

#1 - m19

2023-02-28T02:53:36Z

Even though this scenario is unlikely to ever happen we have confirmed this issue in a test:

function test_sellBond_WithExistingCouponWhenMaxCouponsReached() external { _KUMASwap.sellBond(1); IKUMABondToken.Bond memory bond_ = _bond; for (uint256 i; i < 364; i++) { bond_.coupon = bond_.coupon + 1; _KUMABondToken.issueBond(address(this), bond_); _KUMASwap.sellBond(i + 2); } _KUMABondToken.issueBond(address(this), _bond); _KUMASwap.sellBond(365); }

We intend to fix this.

#2 - GalloDaSballo

2023-03-01T10:41:41Z

The warden has shown a way in which the system could stop selling coupons due to the handling of duplicate coupons, this relies on a specific condition which may not always happen, for this reason, in lack of an attack that can be used to break the functionality, I agree with Medium Severity

#3 - c4-judge

2023-03-01T10:41:45Z

GalloDaSballo marked the issue as selected for report

Findings Information

🌟 Selected for report: 0xsomeone

Also found by: 0x52, bin2chen, hihen

Labels

bug
grade-b
QA (Quality Assurance)
Q-03

Awards

Data not available

External Links

QA Report

Low Risk Findings

[L-01] changePayees can result in broken share count

Description

for (uint256 i; i < newPayees.length; i++) { if (newPayees[i] == address(0)) { revert Errors.CANNOT_SET_TO_ADDRESS_ZERO(); } if (newShares[i] == 0) { revert Errors.SHARE_CANNOT_BE_ZERO(); } address payee = newPayees[i]; _payees.add(payee); _shares[payee] = newShares[i]; _totalShares += newShares[i]; emit PayeeAdded(payee, newShares[i]); }

When adding the newPayees to the payees set the contract fails to validate that each address in newPayees is unique. The results is that _totalShares will be incorrect if there is a repeat in newPayees.

Example: Assume newPayees = [Bob,Bob] and newShares = [5,5]. Since _payees.add(payee) is a non-reverting add, if the address already exists in the set it won't revert. The end result is that _totalShares == 10 but _shares[Bob] == 5. This mismatch causes incorrect distribution of rewards.

Lines of Code

https://github.com/code-423n4/2023-02-kuma/blob/3f3d2269fcb3437a9f00ffdd67b5029487435b95/src/kuma-protocol/KUMAFeeCollector.sol#L152-L189

Mitigation

Require that _payees.add(payee) actually adds the address:

address payee = newPayees[i]; - _payees.add(payee); + require(_payees.add(payee)); _shares[payee] = newShares[i]; _totalShares += newShares[i];

[L-02] expireBond can be called on bonds that are already part of the _expiredBonds set

Description

if (!_bondReserve.contains(tokenId)) { revert Errors.INVALID_TOKEN_ID(); } IKUMAAddressProvider KUMAAddressProvider = _KUMAAddressProvider; if (IKUMABondToken(KUMAAddressProvider.getKUMABondToken()).getBond(tokenId).maturity > block.timestamp) { revert Errors.BOND_NOT_MATURED(); } _expiredBonds.add(tokenId);

expireBond only ever checks if the bond is currently in the reserve set but never checks if it is already part of the expired set

Lines of Code

https://github.com/code-423n4/2023-02-kuma/blob/3f3d2269fcb3437a9f00ffdd67b5029487435b95/src/kuma-protocol/KUMASwap.sol#L321-L337

Mitigation

Revert if bond is already part of the expired set:

- if (!_bondReserve.contains(tokenId)) { + if (!_bondReserve.contains(tokenId) || _expiredBonds.contains(tokenId)) { revert Errors.INVALID_TOKEN_ID(); }

[L-03] KUMASwap#setFees doesn't contain any bounds for fees

Description

KUMASwap#setFees allows manager to set fees to whatever they wish. This includes fees values that would break the functionality of the contract.

Lines of Code

https://github.com/code-423n4/2023-02-kuma/blob/3f3d2269fcb3437a9f00ffdd67b5029487435b95/src/kuma-protocol/KUMASwap.sol#L359-L363

Mitigation

Add reasonable bounds for the max fee allowed

#0 - GalloDaSballo

2023-02-27T10:24:49Z

[L-01] changePayees can result in broken share count Dup 13 [L-02] expireBond can be called on bonds that are already part of the _expiredBonds set L

[L-03] KUMASwap#setFees doesn't contain any bounds for fees L

#1 - GalloDaSballo

2023-03-01T11:03:44Z

1L from dups

3L

#2 - c4-judge

2023-03-01T11:17:19Z

GalloDaSballo marked the issue as grade-b

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