GithubHelp home page GithubHelp logo

2021-05-88mph-findings's People

Contributors

c4-staff avatar code423n4 avatar sockdrawermoney avatar

Watchers

 avatar  avatar  avatar

2021-05-88mph-findings's Issues

Incompatability with deflationary / fee-on-transfer tokens

Handle

cmichel

Vulnerability details

Vulnerability Details

The DInterest.deposit function takes a depositAmount parameter but this parameter is not the actual transferred amount for fee-on-transfer / deflationary (or other rebasing) tokens.

Impact

The actual deposited amount might be lower than the specified depositAmount of the function parameter.
This would lead to wrong interest rate calculations on the principal.

Recommended Mitigation Steps

Transfer the tokens first and compare pre-/after token balances to compute the actual deposited amount.

Use openzeppelin ECDA for erecover

Handle

a_delamo

Vulnerability details

Impact

In Sponsorable.sol is using erecover directly to verify the signature.
Being such a critical piece of the protocol, I would recommend using the ECDSA from openzeppelin as it does more validations when verifying the signature.

 // Currently

 address recoveredAddress =
      ecrecover(digest, sponsorship.v, sponsorship.r, sponsorship.s);
    require(
      recoveredAddress != address(0) && recoveredAddress == sponsorship.sender,
      "Sponsorable: invalid sig"
    );


  //ECDSA

   function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "ECDSA: invalid signature 's' value");
        require(v == 27 || v == 28, "ECDSA: invalid signature 'v' value");

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        require(signer != address(0), "ECDSA: invalid signature");

        return signer;
    }
  

Tools Used

None

Missmatch between the comment and the actual code

Handle

paulius.eth

Vulnerability details

Impact

Here the comment says that it should transfer from msg.sender but it actually transfers from the sender which is not always the msg.sender (e.g. sponsored txs):
// Transfer fundAmount stablecoins from msg.sender
stablecoin.safeTransferFrom(sender, address(this), fundAmount);

Recommended Mitigation Steps

Update the comment to match the code.

Extra precautions in updateAndQuery

Handle

gpersoon

Vulnerability details

Impact

The function updateAndQuery of EMAOracle.sol subtracts the incomeIndex with the previous incomeIndex.
These incomeIndex values are retrieved via the moneyMarket contract from an external contract.

If by accident the previous incomeIndex is larger than the current incomeIndex then the subtraction would be negative and the code halts (reverts), without an error message.
Also the updateAndQuery function would not be able to execute (until the current incomeIndex is larger than the previous incomeIndex).

This situation could occur when an error occurs in one of the current or future money markets.

Proof of Concept

EMAOracle.sol:
function updateAndQuery() {
...
uint256 _lastIncomeIndex = lastIncomeIndex;
...
uint256 newIncomeIndex = moneyMarket.incomeIndex();
uint256 incomingValue =
(newIncomeIndex - _lastIncomeIndex).decdiv(_lastIncomeIndex) /
timeElapsed;

Tools Used

Editor

Recommended Mitigation Steps

Give an error message when the previous incomeIndex is larger than the current incomeIndex.
And/or create a way to recover from this erroneous situation.

Not reverting on failing ERC20 transfer

Handle

Sherlock

Vulnerability details

Impact

In the Visor.sol contract an IERC20 transferFrom() with an arbitrary token argument is used. The return value of the transferFrom() is not used, a boolean indicating if the transaction succeeded. Meaning the transaction could succeed without the actual transfer of tokens happening.

The new entry the time lock is valid even though the tokens are not transferred. User could drain tokens in the contract (e.g. from other users who's transfer did succeed)

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/main/contracts/contracts/visor/Visor.sol#L610

Recommended Mitigation Steps

Use the OpenZeppelin SafeERC20 contract. Changing the call to safeTransferFrom(). This not only makes the transaction revert if the returned boolean is false. But also handles non-standard ERC20 tokens that don’t have boolean return values.

In Visor's case, I recommend using the TransferHelper.safeTransferFrom() as this library is used in other parts of the code.

Gas optimizations - Structs over multiple mappings

Handle

a_delamo

Vulnerability details

Impact

PercentageFeeModel.sol is using multiple mappings for keeping track of the fees.

  struct FeeOverride {
    bool isOverridden;
    uint256 fee;
  }

  mapping(address => FeeOverride) public interestFeeOverrideForPool;
  mapping(address => FeeOverride) public earlyWithdrawFeeOverrideForPool;
  mapping(address => mapping(uint256 => FeeOverride)) public interestFeeOverrideForDeposit;
  mapping(address => mapping(uint256 => FeeOverride)) public earlyWithdrawFeeOverrideForDeposit;

I would recommend using a single mapping with a struct and inside of multiple mappings as is less gas efficient. Here more info: https://medium.com/@novablitz/storing-structs-is-costing-you-gas-774da988895e

Especially, given that some functions are currently accessing multiple mappings.

Also, if fees can not be zero. We could remove the FeeOverride struct to just use uint, but I guess is not possible.

Double-checking 88mph form

Handle

sockdrawermoney

Vulnerability details

Impact

Detailed description of the impact of this finding.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Recommended Mitigation Steps

Multiple definitions of PRECISION

Handle

gpersoon

Vulnerability details

Impact

There are multiple definitions of PRECISION.
This risk is that is someone (a new developer?) would change the value of PRECISION on one location and might forget to change it on one of the other places.

Also 2 of them are defined public while the rest is internal.

Proof of Concept

DInterest.sol: uint256 internal constant PRECISION = 1018;
libs\DecMath.sol: uint256 internal constant PRECISION = 10
18;
models\fee\PercentageFeeModel.sol: uint256 internal constant PRECISION = 1018;
models\interest\LinearDecayInterestModel.sol: uint256 public constant PRECISION = 10
18;
models\interest\LinearInterestModel.sol: uint256 public constant PRECISION = 1018;
models\interest-oracle\EMAOracle.sol: uint256 internal constant PRECISION = 10
18;
models\issuance\MPHIssuanceModel02.sol: uint256 internal constant PRECISION = 1018;
rewards\Vesting02.sol: uint256 internal constant PRECISION = 10
18;
rewards\xMPH.sol: uint256 internal constant PRECISION = 10**18;

Tools Used

grep

Recommended Mitigation Steps

Define PRECISION once and import this in all the other contracts.

Add extra error message in_depositRecordData

Handle

gpersoon

Vulnerability details

Impact

In the function _depositRecordData of DInterest.sol, interestAmount is lowered with feeAmount.
If by accident feeAmount happens to be larger than interestAmount an error occurs and the execution stops, without an error message.
This might make troubleshooting this situation more difficult.

Proof of Concept

DInterest.sol:
function _depositRecordData(..) {
...
interestAmount -= feeAmount;

Tools Used

Editor

Recommended Mitigation Steps

Perhaps add something like:
require(interestAmount >= feeAmount,"DInterest: fee too large");

function payInterestToFunders does not have a re-entrancy modifier

Handle

paulius.eth

Vulnerability details

Impact

function payInterestToFunders does not have a re-entrancy modifier. I expect to see this modifier because similar functions (including sponsored version) have it.

Recommended Mitigation Steps

Add 'nonReentrant' to function payInterestToFunders.

zero amount of token value can be entered for creating vest object in vesting.sol

Handle

Jmukesh

Vulnerability details

Impact

impact will be on the end user , if they by mistake entered the zero amount , they will end paying gas fee for nothing

Proof of Concept

https://github.com/code-423n4/2021-05-88mph/blob/main/contracts/rewards/Vesting.sol#L24

in vest function there is checking of vestPeriodInSeconds but no checking of amount , due to which amount can be set to 0

Tools Used

manual review

Recommended Mitigation Steps

add condition, to check for function parameter

Use immutable keyword

Handle

gpersoon

Vulnerability details

Impact

In EMAOracle.sol several variables are initialized once and never changed. In the comments they are notified as immutable,
however the keyword immutable isn't used.

Proof of Concept

EMAOracle.sol:
contract EMAOracle is IInterestOracle, Initializable {
... /**
Immutable parameters
*/
uint256 public UPDATE_INTERVAL;
uint256 public UPDATE_MULTIPLIER;
uint256 public ONE_MINUS_UPDATE_MULTIPLIER;

function initialize(
..
    UPDATE_INTERVAL = _updateInterval;
..
    UPDATE_MULTIPLIER = updateMultiplier;
    ONE_MINUS_UPDATE_MULTIPLIER = PRECISION - updateMultiplier;

Tools Used

Editor

Recommended Mitigation Steps

Make the variables that are only set once immutable

lack of input validation of timestamp in function _depositRecordData() , Dinternest.sol

Handle

Jmukesh

Vulnerability details

Impact

Due to lack of input validation in of timestamp _depositRecordData() , this function may get failed during execution

Proof of Concept

In Dinterest.sol

https://github.com/code-423n4/2021-05-88mph/blob/main/contracts/DInterest.sol#L796

function _depositRecordData(
address sender,
uint256 depositAmount,
uint256 maturationTimestamp
) internal virtual returns (uint256 depositID, uint256 interestAmount) {
// Ensure input is valid
require(
depositAmount >= MinDepositAmount,
"DInterest: Deposit amount too small"
);
uint256 depositPeriod = maturationTimestamp - block.timestamp;
require(
depositPeriod <= MaxDepositPeriod,
"DInterest: Deposit period too long"
);

here depositPeriod must be >=0 other wise , it will give error due to which function will not excute .

Tools Used

manual review

Recommended Mitigation Steps

add require condition for depositPeriod -->
require(depositPeriod >= 0)

Potential manipulation of `moneyMarket.incomeIndex`

Handle

cmichel

Vulnerability details

Vulnerability Details

The DInsterest contract relies on moneyMarket.incomeIndex() to return the accurate index.
For some implementations, like in yearn, this is the price per share token (vault.pricePerShare()).
There might be cases where this price per share can be temporarily inflated/deflated, for example by depositing underlying tokens to the vault and claiming them back later via a rescue function, or by removing underlyings by taking flashloans (not an issue with Aave).

A similar issue might exist for manipulatable totalValue.

Impact

A temporarily artificially increased incomeIndex would lead to an increased interestAmount in _payInterestToFunders as the interestAmount is multiplied by currentMoneyMarketIncomeIndex = moneyMarket.incomeIndex().
Similarly, a temporarily decreased incomeIndex when funding leads to bigger payouts later as its divided by the intitial recordedMoneyMarketIncomeIndex.

Recommended Mitigation Steps

Each protocol that is integrated must be checked that the incomeIndex/totalValue functions cannot be manipulated.

Unchecking the ownership of `mph` in function `distributeFundingRewards` could cause several critical functions to revert

Handle

shw

Vulnerability details

Impact

In contract MPHMinter, the function distributeFundingRewards does not check whether the contract itself is the owner of mph. If the contract is not the owner of mph, mph.ownerMint could revert, causing functions such as withdraw, rolloverDeposit, payInterestToFunders in the contract DInterest to revert as well.

Proof of Concept

Referenced code:
MPHMinter.sol#L121
DInterest.sol#L1253
DInterest.sol#L1420

Tools Used

None

Recommended Mitigation Steps

Add a mph.owner() != address(this) check as in the other functions (e.g., mintVested).

Longer maturity leads to lower returns

Handle

cmichel

Vulnerability details

Vulnerability Details

The LinearDecayInterestModel computes the interestAmount on a deposit as depositAmount * moneyMarketInterestRatePerSecond * IRMultiplier * depositPeriodInSeconds where the IRMultiplier is a function of the depositPeriodInSeconds computed in:

function getIRMultiplier(uint256 depositPeriodInSeconds)
    public
    view
    returns (uint256)
{
    uint256 multiplierDecrease = depositPeriodInSeconds * multiplierSlope;
    if (multiplierDecrease >= multiplierIntercept) {
        return 0;
    } else {
        return multiplierIntercept - multiplierDecrease;
    }
}

The higher the duration the lower the interest amount, if the duration is long enough no interest will be returned.

Impact

I'd expect longer durations to lead to a higher percentage on the principal due to opportunity cost on the money.
If I take a loan with a long maturity (multiplierDecrease >= multiplierIntercept) I get zero interest. Why would anyone do that?

Recommended Mitigation Steps

I might not understand what the intention is or the multiplier is wrong.

Gas optimizations - cache chainId

Handle

a_delamo

Vulnerability details

Impact

In Sponsorable, the method _validateSponsorship is calling all the time

    uint256 chainId;
    assembly {
      chainId := chainid()
    }

Being the chainId "static", we could keep the value cached and just refresh it from time to time?

Running on Remix the following code gives 134 gas cost.

function chain() external {
        uint256 chainId;
        assembly {
          chainId := chainid()
        }
    }

Tools Used

None

Anyone can withdraw vested amount on behalf of someone

Handle

cmichel

Vulnerability details

Vulnerability Details

The Vesting.withdrawVested function allows withdrawing the tokens of other users.
While the tokens are sent to the correct address, this can lead to issues with smart contracts that might rely on claiming the tokens themselves.

As one example, suppose the _to address corresponds to a smart contract that has a function of the following form:

function withdrawAndDoSomething() {
    contract.withdrawVested(address(this), amount);
    token.transfer(externalWallet, amount)
}

Impact

If the contract has no other functions to transfer out funds, they may be locked forever in this contract.

Recommended Mitigation Steps

Do not allow users to withdraw on behalf of other users.

Gas optimizations by using external over public

Handle

a_delamo

Vulnerability details

Impact

Some functions could use external instead of public in order to save gas.

If we run the following methods on Remix, we can see the difference

  //  transaction cost  21448 gas
  //  execution cost    176 gas
  function tt() external returns(uint256) {
      return 0;
  }

  //  transaction cost  21558 gas
  //  execution cost    286 gas
  function tt_public() public returns(uint256) {
      return 0;
  }
surplusOfDeposit(uint256) should be declared external:
        - DInterest.surplusOfDeposit(uint256) (contracts/DInterest.sol#623-648)
dividendOf(uint256,address,address) should be declared external:
        - ERC1155DividendToken.dividendOf(uint256,address,address) (contracts/libs/ERC1155DividendToken.sol#101-107)
withdrawnDividendOf(uint256,address,address) should be declared external:
        - ERC1155DividendToken.withdrawnDividendOf(uint256,address,address) (contracts/libs/ERC1155DividendToken.sol#114-126)
uri(uint256) should be declared external:
        - ERC1155Upgradeable.uri(uint256) (contracts/libs/ERC1155Upgradeable.sol#92-94)
balanceOfBatch(address[],uint256[]) should be declared external:
        - ERC1155Upgradeable.balanceOfBatch(address[],uint256[]) (contracts/libs/ERC1155Upgradeable.sol#124-143)
setApprovalForAll(address,bool) should be declared external:
        - ERC1155Upgradeable.setApprovalForAll(address,bool) (contracts/libs/ERC1155Upgradeable.sol#148-160)
safeTransferFrom(address,address,uint256,uint256,bytes) should be declared external:
        - ERC1155Upgradeable.safeTransferFrom(address,address,uint256,uint256,bytes) (contracts/libs/ERC1155Upgradeable.sol#178-190)
safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) should be declared external:
        - ERC1155Upgradeable.safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) (contracts/libs/ERC1155Upgradeable.sol#195-207)
increaseAllowance(address,uint256) should be declared external:
        - ERC20Wrapper.increaseAllowance(address,uint256) (contracts/libs/ERC20Wrapper.sol#146-157)
decreaseAllowance(address,uint256) should be declared external:
        - ERC20Wrapper.decreaseAllowance(address,uint256) (contracts/libs/ERC20Wrapper.sol#173-186)
mint(address,uint256) should be declared external:
        - ERC20Mock.mint(address,uint256) (contracts/mocks/ERC20Mock.sol#7-9)
deposit(uint256) should be declared external:
        - VaultMock.deposit(uint256) (contracts/mocks/VaultMock.sol#16-21)
withdraw(uint256) should be declared external:
        - VaultMock.withdraw(uint256) (contracts/mocks/VaultMock.sol#23-29)
deposit(uint256) should be declared external:
        - VaultWithDepositFeeMock.deposit(uint256) (contracts/mocks/VaultWithDepositFeeMock.sol#23-33)
withdraw(uint256) should be declared external:
        - VaultWithDepositFeeMock.withdraw(uint256) (contracts/mocks/VaultWithDepositFeeMock.sol#35-41)
updateAndQuery() should be declared external:
        - EMAOracle.updateAndQuery() (contracts/models/interest-oracle/EMAOracle.sol#55-84)
query() should be declared external:
        - EMAOracle.query() (contracts/models/interest-oracle/EMAOracle.sol#86-88)
initialize() should be declared external:
        - MPHToken.initialize() (contracts/rewards/MPHToken.sol#19-23)
ownerMint(address,uint256) should be declared external:
        - MPHToken.ownerMint(address,uint256) (contracts/rewards/MPHToken.sol#26-33)

Tools Used

Slither

Gas optimizations - storage over memory

Handle

a_delamo

Vulnerability details

Impact

Some functions are using memory to read state variables when using storage is more gas efficient.

function withdrawableAmountOfDeposit(
        uint256 depositID,
        uint256 virtualTokenAmount
    ) external view returns (uint256 withdrawableAmount, uint256 feeAmount) {
        // Verify input
        //FIXME: Storage
        Deposit memory depositEntry = _getDeposit(depositID);
        if (virtualTokenAmount == 0) {
            return (0, 0);
        } else {
            if (virtualTokenAmount > depositEntry.virtualTokenTotalSupply) {
                virtualTokenAmount = depositEntry.virtualTokenTotalSupply;
            }
        }
    ...
}



  function _getVestWithdrawableAmount(uint256 vestID)
    internal
    view
    returns (uint256 withdrawableAmount)
  {
    // read vest data
    //FIXME: Storage
    Vest memory vestEntry = _getVest(vestID);
    DInterest pool = DInterest(vestEntry.pool);
    DInterest.Deposit memory depositEntry =
      pool.getDeposit(vestEntry.depositID);
   
   ...
   }

The following functions contains memory than could be changed to storage:

  • _computeAndUpdateFundingInterestAfterWithdraw
  • _topupDepositRecordData
  • _withdrawRecordData
  • withdrawableAmountOfDeposit
  • getInterestFeeAmount
  • getEarlyWithdrawFeeAmount
  • _getVestWithdrawableAmount

https://docs.soliditylang.org/en/v0.4.21/types.html#reference-types

Recommended Mitigation Steps

Test issue

Handle

adamavenir

Vulnerability details

Impact

Detailed description of the impact of this finding.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Recommended Mitigation Steps

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.