Malt Finance contest - gpersoon's results

Yield farmable, incentive-centric algorithmic stable coin.

General Information

Platform: Code4rena

Start Date: 25/11/2021

Pot Size: $80,000 USDC

Total HM: 35

Participants: 32

Period: 7 days

Judge: GalloDaSballo

Total Solo HM: 27

Id: 59

League: ETH

Malt Finance

Findings Distribution

Researcher Performance

Rank: 13/32

Findings: 3

Award: $1,696.10

🌟 Selected for report: 5

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: gpersoon

Also found by: ScopeLift, leastwood

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

298.0144 USDC - $298.01

External Links

Handle

gpersoon

Vulnerability details

Impact

The function bondToAccount() of Bonding.sol has a check based on _notSameBlock() _notSameBlock() makes sure the same msg.sender cannot do 2 actions within the same block.

However this can be circumvented in this case: Suppose you call bondToAccount() via a (custom) smart contract, then the msg.sender will be the address of the smart contract. For a pseudo code proof of concept see below.

I'm not sure what the deeper reason is for the _notSameBlock() in bondToAccount(). But if it is important then circumventing this check it will pose a risk.

Proof of Concept

call function attack1.attack()

contract attack1 {
   function attack(address account, uint256 amount) {
         call attack2.forward(account, amount);
         call any other function of malt
  }
}

contract attack2 {
   function forward(address account, uint256 amount) {
       call bonding.bondToAccount(account, amount); // uses msg.sender of attack2
   }
}

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Bonding.sol#L81-L92

function bondToAccount(address account, uint256 amount) public {
    if (msg.sender != offering) {
         _notSameBlock();
    }
    ...

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Permissions.sol#L135-L141

function _notSameBlock() internal {
    require( block.number > lastBlock[_msgSender()],"Can't carry out actions in the same block" );
    lastBlock[_msgSender()] = block.number;
  }

Tools Used

Add access controls to the function bondToAccount() An end-user could still call bond()

#0 - GalloDaSballo

2022-01-09T23:34:16Z

notSameBlock is effectively being used as the nonReentrant modifier, without the same security guarantees, as such, in spite of not having a specific attack vector, because the warden showed how to side step this security feature of the protocol, am going to raise the severity to Medium

Findings Information

🌟 Selected for report: gpersoon

Also found by: 0x1f8b

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

496.6906 USDC - $496.69

External Links

Handle

gpersoon

Vulnerability details

Impact

The function setAdvanceIncentive of DAO.sol doesn't check for a maximum value of incentive. If incentivewould be very large, then advanceIncentive would be very large and the function advance() would mint a large amount of malt.

The function setAdvanceIncentive() can only be called by an admin, but a mistake could be made. Also if an admin would want to do a rug pull, this would be an ideal place to do it.

Proof of Concept

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DAO.sol#L98-L104

  function setAdvanceIncentive(uint256 incentive)  externalonlyRole(ADMIN_ROLE, "Must have admin role") {
   ...
    advanceIncentive = incentive;

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DAO.sol#L55-L63

function advance() external {
...
    malt.mint(msg.sender, advanceIncentive * 1e18);

Tools Used

Check for a reasonable maximum value in advance()

#0 - 0xScotch

2021-12-03T19:40:21Z

Definitely need to guard against arbitrarily large incentives. Disagree the risk is medium though.

#1 - GalloDaSballo

2022-01-22T15:07:17Z

Agree with the finding, this is an example of admin privilege, where the admin can set a variable which can be used to dilute the token and rug the protocol.

Because this is contingent on the admin's action, I believe medium severity to be proper

#2 - GalloDaSballo

2022-01-30T16:41:57Z

The simple rationale on the medium severity is that the owner could set the incentive to an exorbitant amount with the goal of minting a lot of tokens for an exit scam

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

367.919 USDC - $367.92

External Links

Handle

gpersoon

Vulnerability details

Impact

The function setSampleMemory of MovingAverage.sol takes the modulo of counter with the new value of _sampleMemory: "counter = counter % _sampleMemory;"

Suppose: counter =15 ; sampleMemory=10 and _sampleMemory=12 Then: counter = counter % _sampleMemory ==> 3, which means processing will continue at position 3.

However I think it should use: counter = counter % sampleMemory, so it will continue at position 5

Proof of Concept

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/MovingAverage.sol#L424-L442

function setSampleMemory(uint256 _sampleMemory) external onlyRole(ADMIN_ROLE, "Must have admin privs")  {
  ...
    if (_sampleMemory > sampleMemory) {
      ...
      counter = counter % _sampleMemory;
    } else {
   }
    sampleMemory = _sampleMemory;
}

Tools Used

Doublecheck the theory above and if you agree: change

 counter = counter % _sampleMemory;

to

 counter = counter %  sampleMemory;

#0 - GalloDaSballo

2022-01-24T01:09:47Z

The sponsor agrees with the finding so we'll allow

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

367.919 USDC - $367.92

External Links

Handle

gpersoon

Vulnerability details

Impact

The function setStabilityThresholds of StabilizerNode.sol set the values for upperStabilityThreshold and lowerStabilityThreshold, however there is no check for a maximum value. This means that in function _shouldAdjustSupply() the values for upperThreshold and lowerThreshold could get larger than priceTarget. When they are subtracted from priceTarget a revert will occur.

Thus it is useful the make sure that upperStabilityThreshold and lowerStabilityThreshold don't get too large.

Proof of Concept

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/StabilizerNode.sol#L445-L454

function setStabilityThresholds(uint256 _upper, uint256 _lower) external onlyRole(ADMIN_ROLE, "Must have admin role") {
    require(_upper > 0 && _lower > 0, "Must be above 0");
    upperStabilityThreshold = _upper;
    lowerStabilityThreshold = _lower;

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/StabilizerNode.sol#L198-L206

function _shouldAdjustSupply(uint256 exchangeRate) internal view returns (bool) {
   ...
    uint256 upperThreshold = priceTarget.mul(upperStabilityThreshold).div(10**decimals); // upperStabilityThreshold could be > 10**dec => upperThreshold could be > priceTarget
    uint256 lowerThreshold = priceTarget.mul(lowerStabilityThreshold).div(10**decimals);  // lowerStabilityThreshold could be > 10**dec => lowerThreshold could be > priceTarget

    return (exchangeRate <= priceTarget.sub(lowerThreshold) && !auction.auctionActive(auction.currentAuctionId())) || exchangeRate >= priceTarget.add(upperThreshold); // can revert
  }

Tools Used

In function setStabilityThresholds() check for a maximum value of upperStabilityThreshold and lowerStabilityThreshold

#0 - GalloDaSballo

2022-01-25T01:39:30Z

Agree with the finding, as with any admin privilege, providing clear limits gives better guarantees to protocol users. Because there's no specific attack vector, I agree with low severity

Findings Information

🌟 Selected for report: gpersoon

Also found by: WatchPug

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

165.5635 USDC - $165.56

External Links

Handle

gpersoon

Vulnerability details

Impact

The function setAuctionAverageLookback of AuctionBurnReserveSkew.sol change auctionAverageLookback However there is also the variable "count" that is used in amongst others, addAbovePegObservation(). The modulo of count with auctionAverageLookback is calculated via _getIndexOfObservation(). When you change auctionAverageLookback then the modulo will result in a different value, so you end up in a different location of the circular buffer.

You should probably adapt count as well in the function setAuctionAverageLookback() (see also function setSampleMemory of MovingAverage.sol where a similar pattern is used)

Proof of Concept

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionBurnReserveSkew.sol#L186-L200

function setAuctionAverageLookback(uint256 _lookback) external onlyRole(ADMIN_ROLE, "Must have admin role") {
..
    if (_lookback > auctionAverageLookback) {
      for (uint i = auctionAverageLookback; i < _lookback; i++) {
        pegObservations.push(0);
      }
    }

    auctionAverageLookback = _lookback;
 ...

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionBurnReserveSkew.sol#L143-L153

 function addAbovePegObservation(uint256 amount)  public onlyRole(STABILIZER_NODE_ROLE, "Must be a stabilizer node to call this method") {
    uint256 index = _getIndexOfObservation(count);
    ...
    pegObservations[index] = 1;
    count = count + 1;
...

https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionBurnReserveSkew.sol#L134-L136

 function _getIndexOfObservation(uint _index) internal view returns (uint index) {
    return _index % auctionAverageLookback;
  }

Tools Used

Doublecheck the theory above and if you agree: Add the following statement in the function setAuctionAverageLookback(), before auctionAverageLookback is updated.

 count = count  % auctionAverageLookback ; // the old version of auctionAverageLookback 

#0 - GalloDaSballo

2022-01-25T02:31:43Z

Sponsor confirms, technically the issue can be sidestepped by doing the math and then setting w/e value is needed, as such low severity is appropriate

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