InsureDAO contest - cmichel's results

Anyone can create an insurance pool like Uniswap.

General Information

Platform: Code4rena

Start Date: 07/01/2022

Pot Size: $80,000 USDC

Total HM: 21

Participants: 37

Period: 7 days

Judge: 0xean

Total Solo HM: 14

Id: 71

League: ETH

InsureDAO

Findings Distribution

Researcher Performance

Rank: 5/37

Findings: 7

Award: $3,932.87

🌟 Selected for report: 6

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: cmichel

Also found by: WatchPug

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

1709.7813 INSURE - $598.42

1038.0815 USDC - $1,038.08

External Links

Handle

cmichel

Vulnerability details

Note that the PoolTemplate.initialize function, called when creating a market with Factory.createMarket, calls a vault function to transfer an initial deposit amount (conditions[1]) from the initial depositor (_references[4]):

// PoolTemplate
function initialize(
     string calldata _metaData,
     uint256[] calldata _conditions,
     address[] calldata _references
) external override {
     // ...

     if (_conditions[1] > 0) {
          // @audit vault calls asset.transferFrom(_references[4], vault, _conditions[1])
          _depositFrom(_conditions[1], _references[4]);
     }
}

function _depositFrom(uint256 _amount, address _from)
     internal
     returns (uint256 _mintAmount)
{
     require(
          marketStatus == MarketStatus.Trading && paused == false,
          "ERROR: DEPOSIT_DISABLED"
     );
     require(_amount > 0, "ERROR: DEPOSIT_ZERO");

     _mintAmount = worth(_amount);
     // @audit vault calls asset.transferFrom(_from, vault, _amount)
     vault.addValue(_amount, _from, address(this));

     emit Deposit(_from, _amount, _mintAmount);

     //mint iToken
     _mint(_from, _mintAmount);
}

The initial depositor needs to first approve the vault contract for the transferFrom to succeed.

An attacker can then frontrun the Factory.createMarket transaction with their own market creation (it does not have access restrictions) and create a market with different parameters but still passing in _conditions[1]=amount and _references[4]=victim.

A market with parameters that the initial depositor did not want (different underlying, old whitelisted registry/parameter contract, etc.) can be created with their tokens and these tokens are essentially lost.

Can the initial depositor be set to Factory.createMarket's msg.sender, instead of being able to pick a whitelisted one as _references[4]?

Findings Information

🌟 Selected for report: cmichel

Also found by: Ruhum, WatchPug, camden

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

692.4614 INSURE - $242.36

420.423 USDC - $420.42

External Links

Handle

cmichel

Vulnerability details

The Vault.withdrawRedundant has wrong logic that allows the admins to steal the underlying vault token.

function withdrawRedundant(address _token, address _to)
     external
     override
     onlyOwner
{
     if (
          _token == address(token) &&
          balance < IERC20(token).balanceOf(address(this))
     ) {
          uint256 _redundant = IERC20(token).balanceOf(address(this)) -
               balance;
          IERC20(token).safeTransfer(_to, _redundant);
     } else if (IERC20(_token).balanceOf(address(this)) > 0) {
          // @audit they can rug users. let's say balance == IERC20(token).balanceOf(address(this)) => first if false => transfers out everything
          IERC20(_token).safeTransfer(
               _to,
               IERC20(_token).balanceOf(address(this))
          );
     }
}
POC
  • Vault deposits increase as Vault.addValue is called and the balance increases by _amount as well as the actual IERC20(token).balanceOf(this). Note that balance == IERC20(token).balanceOf(this)
  • Admins call vault.withdrawRedundant(vault.token(), attacker) which goes into the else if branch due to the balance inequality condition being false. It will transfer out all vault.token() amounts to the attacker.

Impact

There's a backdoor in the withdrawRedundant that allows admins to steal all user deposits.

I think the devs wanted this logic from the code instead:

function withdrawRedundant(address _token, address _to)
     external
     override
     onlyOwner
{
     if (
          _token == address(token)
     ) {
          if (balance < IERC20(token).balanceOf(address(this))) {
               uint256 _redundant = IERC20(token).balanceOf(address(this)) -
                    balance;
               IERC20(token).safeTransfer(_to, _redundant);
          }
     } else if (IERC20(_token).balanceOf(address(this)) > 0) {
          IERC20(_token).safeTransfer(
               _to,
               IERC20(_token).balanceOf(address(this))
          );
     }
}

#0 - oishun1112

2022-02-02T09:07:53Z

similar to PVE03 (Peckshield audit)

#1 - oishun1112

2022-02-02T09:13:07Z

We will create a PR and merge after we merge both audit/code4rena and audit/peckshield branches in the InsureDAO repository.

Findings Information

🌟 Selected for report: WatchPug

Also found by: Dravee, Ruhum, cmichel, pmerkleplant

Labels

bug
duplicate
2 (Med Risk)

Awards

149.5717 INSURE - $52.35

90.8114 USDC - $90.81

External Links

Handle

cmichel

Vulnerability details

Certain ERC20 tokens make modifications to their ERC20's transfer or balanceOf functions. One type of these tokens is deflationary tokens that charge a certain fee for every transfer() or transferFrom().

Impact

The Vault.addValue(Batch) functions will recive less tokens than specified because they receive the amount - fees:

function addValueBatch(
     uint256 _amount,
     address _from,
     address[2] memory _beneficiaries,
     uint256[2] memory _shares
) external override onlyMarket returns (uint256[2] memory _allocations) {
     // ...
     IERC20(token).safeTransferFrom(_from, address(this), _amount);

     // @audit does not work with fee-on-transfer
     balance += _amount;
     // ...
}

One possible mitigation is to measure the asset change right before and after the asset-transferring calls.

Findings Information

🌟 Selected for report: p4st13r4

Also found by: 0xngndev, Fitraldys, cmichel, pauliax, tqts

Labels

bug
duplicate
1 (Low Risk)

Awards

37.3929 INSURE - $13.09

22.7028 USDC - $22.70

External Links

Handle

cmichel

Vulnerability details

The CDSTemplate.withdraw function stores the withdrawalReq in a memory structure and reduces the amount on the memory variable instead of the storage variable:

function withdraw(uint256 _amount) external returns (uint256 _retVal) {
     Withdrawal memory request = withdrawalReq[msg.sender];
     // ...

     // @audit request is memory
     request.amount -= _amount;

     //Burn iToken
     _burn(msg.sender, _amount);

     //Withdraw liquidity
     crowdPool -= vault.withdrawValue(_retVal, msg.sender);
     emit Withdraw(msg.sender, _amount, _retVal);
}

Impact

The user can circumvent the lockup for repeated deposits as the same withdrawalReq with the unchanged amount can just be reused over and over again.

Replace request.amount -= _amount; with withdrawalReq[msg.sender].amount -= _amount;.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor acknowledged

Awards

379.9514 INSURE - $132.98

230.6848 USDC - $230.68

External Links

Handle

cmichel

Vulnerability details

The Factory.createMarket function copies the conditions from the conditionlist only according to the length of provided _conditions parameter:

if (_conditions.length > 0) {
     for (uint256 i = 0; i < _conditions.length; i++) {
          if (conditionlist[address(_template)][i] > 0) {
          // @audit can only partially use conditions by passing _conditions.length < conditionlist[..].length
          _conditions[i] = conditionlist[address(_template)][i];
          }
     }
}

Impact

The user can create markets that don't use all (or none) of the conditions for the template. If they only want condition at index 2 they cannot do that as it will always also copy conditions 0 and 1.

Either always use all conditions or allow a more selective choice of conditions that does not require using all conditions i < j when condition j is desired.

#0 - oishun1112

2022-01-20T09:20:41Z

we keep here as it is. conditions[] is like a anything box. it can allow arbitrary parameter from user, and also defined parameter by owner.

#1 - oishun1112

2022-01-20T09:22:43Z

it can be useful to set a parameter of Template without changing the place to execute the set function.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
resolved
sponsor confirmed

Awards

379.9514 INSURE - $132.98

230.6848 USDC - $230.68

External Links

Handle

cmichel

Vulnerability details

The Ownership.acceptTransferOwnership function does not reset _futureOwner to zero.

Impact

The future owner can repeatedly accept the governance, emitting an AcceptNewOwnership event each time, bloating listeners for this event with unnecessary data.

Reset _futureOwner to zero in acceptTransferOwnership.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
resolved
sponsor confirmed
sponsor disputed

Awards

379.9514 INSURE - $132.98

230.6848 USDC - $230.68

External Links

Handle

cmichel

Vulnerability details

The Parameters.setLowerSlack/setUpperSlack functions do not check that the new value does still satisfy the _lowerSlack <= _upperSlack condition.

Check that _lowerSlack <= _upperSlack is still satisfied in these functions.

#0 - aoki0430

2022-01-22T07:00:32Z

What should we do on this issue, add require(_lowerSlack <= _upperSlack, "ERROR: EXCEED_UPPER_SLACK" or lower...) to inside the two functions, right? @oishun1112

#1 - oishun1112

2022-01-22T08:14:47Z

@aoki0430 yes, something like that. Keep in mind that lowerSlack must be lower than upperSlack

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
disagree with severity
sponsor acknowledged

Awards

379.9514 INSURE - $132.98

230.6848 USDC - $230.68

External Links

Handle

cmichel

Vulnerability details

The Index pool allows depositing / withdrawing and the amount is a share on the totalLiquidity = vault.underlyingValue(address(this)) + _accruedPremiums();. The premiums are increased only when a PoolTemplate.invest() transaction is run.

An attacker can therefore steal part of the premiums by:

  1. Depositing into an index with IndexTemplate.deposit right before the PoolTemplate.invest() transaction. Request the withdrawal with requestWithdraw
  2. Withdraw after the transaction again (or after the minimum withdrawal time passed) using IndexTemplate.withdraw

Make sure _lockup = parameters.getLockup(msg.sender); is a reasonable long time to make these premium timing attacks less interesting. Alternatively, stream the premium over time to every depositor instead of as a single big reward to all current depositors.

#0 - oishun1112

2022-01-18T09:39:45Z

I believe PoolTemplate.invest() => PoolTemplate.insure()

#1 - oishun1112

2022-01-18T09:47:52Z

It is ok to front-run insure() with deposit(). This action

  • reduces premium of buyer
  • reduces income of underwriters.
  • reduces risk of other underwriters (reduces amount of payment when insurance is redeemed)

front-runner have to take a risk of being locked (in case withdraw-able amount become less than his deposit)

he can wait for big transaction, and front-run it. This brings a lot of income in a moment, but taking very high risk. It sounds fair game to me.

#2 - oishun1112

2022-01-18T09:49:25Z

So this is true but not an issue. Yes, we are having at least a week of lockup period at the beggining

#3 - 0xean

2022-01-27T22:07:23Z

moving to low-risk .

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