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
Rank: 5/37
Findings: 7
Award: $3,932.87
π Selected for report: 6
π Solo Findings: 0
1709.7813 INSURE - $598.42
1038.0815 USDC - $1,038.08
cmichel
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]
?
#0 - oishun1112
2022-02-02T09:25:20Z
692.4614 INSURE - $242.36
420.423 USDC - $420.42
cmichel
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)) ); } }
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)
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.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.
π Selected for report: WatchPug
Also found by: Dravee, Ruhum, cmichel, pmerkleplant
149.5717 INSURE - $52.35
90.8114 USDC - $90.81
cmichel
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()
.
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.
#0 - oishun1112
2022-01-19T05:11:19Z
37.3929 INSURE - $13.09
22.7028 USDC - $22.70
cmichel
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); }
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;
.
#0 - oishun1112
2022-01-13T17:20:42Z
π Selected for report: cmichel
379.9514 INSURE - $132.98
230.6848 USDC - $230.68
cmichel
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]; } } }
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.
π Selected for report: cmichel
379.9514 INSURE - $132.98
230.6848 USDC - $230.68
cmichel
The Ownership.acceptTransferOwnership
function does not reset _futureOwner
to zero.
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
.
π Selected for report: cmichel
379.9514 INSURE - $132.98
230.6848 USDC - $230.68
cmichel
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
#2 - kohshiba
2022-02-07T09:18:31Z
π Selected for report: cmichel
379.9514 INSURE - $132.98
230.6848 USDC - $230.68
cmichel
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:
IndexTemplate.deposit
right before the PoolTemplate.invest()
transaction. Request the withdrawal with requestWithdraw
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
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 .