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: 1/37
Findings: 24
Award: $25,579.68
🌟 Selected for report: 23
🚀 Solo Findings: 10
1709.7813 INSURE - $598.42
1038.0815 USDC - $1,038.08
WatchPug
function initialize( string calldata _metaData, uint256[] calldata _conditions, address[] calldata _references ) external override { require( initialized == false && bytes(_metaData).length > 0 && _references[0] != address(0) && _references[1] != address(0) && _references[2] != address(0) && _references[3] != address(0) && _references[4] != address(0) && _conditions[0] <= _conditions[1], "ERROR: INITIALIZATION_BAD_CONDITIONS" ); initialized = true; string memory _name = string( abi.encodePacked( "InsureDAO-", IERC20Metadata(_references[1]).name(), "-PoolInsurance" ) ); string memory _symbol = string( abi.encodePacked("i-", IERC20Metadata(_references[1]).symbol()) ); uint8 _decimals = IERC20Metadata(_references[0]).decimals(); initializeToken(_name, _symbol, _decimals); registry = IRegistry(_references[2]); parameters = IParameters(_references[3]); vault = IVault(parameters.getVault(_references[1])); metadata = _metaData; marketStatus = MarketStatus.Trading; if (_conditions[1] > 0) { _depositFrom(_conditions[1], _references[4]); } }
Anyone can create a pool from a whitelisted template and can specify certain parameters.
However, the parameters passed to PoolTemplate.sol#initialize()
can be used to deposit from a specified address as long as they have approved the Vault contract before.
templates[address(_template)].approval
== truereflist[address(_template)][4][address(0)]
= trueUSDC
for uint256.max
to the Vault contract;Factory#createMarket()
and set _conditions[1]
= 10,000 and _references[4]
= Alice's address;Users' allowances can be used by anyone.
Change to:
function initialize( string calldata _metaData, uint256[] calldata _conditions, address[] calldata _references ) external override { require( initialized == false && bytes(_metaData).length > 0 && _references[0] != address(0) && _references[1] != address(0) && _references[2] != address(0) && _references[3] != address(0) && _references[4] != address(0) && _conditions[0] <= _conditions[1], "ERROR: INITIALIZATION_BAD_CONDITIONS" ); initialized = true; string memory _name = string( abi.encodePacked( "InsureDAO-", IERC20Metadata(_references[1]).name(), "-PoolInsurance" ) ); string memory _symbol = string( abi.encodePacked("i-", IERC20Metadata(_references[1]).symbol()) ); uint8 _decimals = IERC20Metadata(_references[0]).decimals(); initializeToken(_name, _symbol, _decimals); registry = IRegistry(_references[2]); parameters = IParameters(_references[3]); vault = IVault(parameters.getVault(_references[1])); metadata = _metaData; marketStatus = MarketStatus.Trading; if (_conditions[1] > 0) { _depositFrom(_conditions[1], tx.origin); } }
#0 - oishun1112
2022-01-19T10:36:54Z
I don't want to use tx.origin
#1 - oishun1112
2022-01-19T10:37:06Z
#2 - CloudEllie
2022-02-21T22:38:58Z
Noting here that post-awarding, the judge @0xean reviewed additional input from the sponsor and determined that this issue should be treated as a duplicate of #250 for the purposes of the report.
#3 - 0xean
2022-02-22T14:00:02Z
confirming dupe of #250
692.4614 INSURE - $242.36
420.423 USDC - $420.42
WatchPug
Based on the context, withdrawRedundant()
intends to disallow the owner to withdraw more Vault tokens than the surplus amount.
However, the current implementation is wrong, which allows the Vault owner to drain all the funds.
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) { IERC20(_token).safeTransfer( _to, IERC20(_token).balanceOf(address(this)) ); } }
If the owner's private key is compromised, the attacker can:
When token.balanceOf(address(this)) == balance
:
withdrawRedundant()
, withdraw all the funds.When token.balanceOf(address(this)) > balance
:
withdrawRedundant()
, withdraw surplus amount;withdrawRedundant()
, withdraw all the funds.When token.balanceOf(address(this)) < balance
:
balance - token.balanceOf(address(this))
to the Vault contract;withdrawRedundant()
, withdraw all the funds.Change to:
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 ( _token != address(token) && IERC20(_token).balanceOf(address(this)) > 0 ) { IERC20(_token).safeTransfer( _to, IERC20(_token).balanceOf(address(this)) ); } }
#0 - oishun1112
2022-01-19T06:34:49Z
🌟 Selected for report: WatchPug
3799.5139 INSURE - $1,329.83
2306.8477 USDC - $2,306.85
WatchPug
The current design/implementation allows a market
address (registered on registry
) to call Vault#addValue()
and transfer tokens from an arbitrary address to a specified _beneficiary
up the approved amount at any time, and the _beneficiary
can withdraw the funds by calling Vault#withdrawAllAttribution()
immediately.
This poses a very dangerous risk to all the users that approved their tokens to the Vault contracts (each one holds all users' allowances for that token).
modifier onlyMarket() { require( IRegistry(registry).isListed(msg.sender), "ERROR_ONLY_MARKET" ); _; }
function addValue( uint256 _amount, address _from, address _beneficiary ) external override onlyMarket returns (uint256 _attributions) { if (totalAttributions == 0) { _attributions = _amount; } else { uint256 _pool = valueAll(); _attributions = (_amount * totalAttributions) / _pool; } IERC20(token).safeTransferFrom(_from, address(this), _amount); balance += _amount; totalAttributions += _attributions; attributions[_beneficiary] += _attributions; }
Registry owner can call Registry#supportMarket()
and mark an arbitrary address as a market
.
function supportMarket(address _market) external override { require(!markets[_market], "ERROR: ALREADY_REGISTERED"); require( msg.sender == factory || msg.sender == ownership.owner(), "ERROR: UNAUTHORIZED_CALLER" ); require(_market != address(0), "ERROR: ZERO_ADDRESS"); allMarkets.push(_market); markets[_market] = true; emit NewMarketRegistered(_market); }
Or, the owner of the Factory can call createMarket()
to add a malicous market contract via a custom template contract to the markets
list.
A malicious/compromised Registry owner can:
Registry#supportMarket()
and set markets[attackerAddress]
to true
;Vault#addValue(token.balanceOf(victimAddress), victimAddress, attackerAddress)
and transferring all the balanceOf victim's wallet to the vault, owned by attackerAddress
.Vault#withdrawAllAttribution(attackerAddress)
and retrive the funds.The malicious/compromised Registry owner can repeat the steps above for all the users who approved the Vault contract for all the Vault contracts.
As a result, the attacker can steal all the wallet balances of the tokens approved to the protocol.
Improper access control for using users' allowances.
Consider changing the design/implementation to make sure that the allowances approved by the users can only be used by themselves.
#0 - oishun1112
2022-01-20T07:36:36Z
this is an issue only when ownership control has fail. This architecture is necessary to achieve simplicity of the code. We assume ownership control works fine.
#1 - 0xean
2022-01-27T15:19:57Z
Agree with warden that the privilege addresses should not be able to use approvals in a way that rugs users funds.
Based on the fact that we have seen many rug pulls in the space based on compromised "owner" keys, this is a valid attack path.
3 — High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals).
🌟 Selected for report: WatchPug
3799.5139 INSURE - $1,329.83
2306.8477 USDC - $2,306.85
WatchPug
Precision loss while converting between the amount of shares
and the amount of underlying tokens
back and forth is not handled properly.
uint256 _shortage; if (totalLiquidity() < _amount) { //Insolvency case _shortage = _amount - _value; uint256 _cds = ICDSTemplate(registry.getCDS(address(this))) .compensate(_shortage); _compensated = _value + _cds; } vault.offsetDebt(_compensated, msg.sender);
In the current implementation, when someone tries to resume the market after a pending period ends by calling PoolTemplate.sol#resume()
, IndexTemplate.sol#compensate()
will be called internally to make a payout. If the index pool is unable to cover the compensation, the CDS pool will then be used to cover the shortage.
However, while CDSTemplate.sol#compensate()
takes a parameter for the amount of underlying tokens, it uses vault.transferValue()
to transfer corresponding _attributions
(shares) instead of underlying tokens.
Due to precision loss, the _attributions
transferred in the terms of underlying tokens will most certainly be less than the shortage.
At L444, the contract believes that it's been compensated for _value + _cds
, which is lower than the actual value, due to precision loss.
At L446, when it calls vault.offsetDebt(_compensated, msg.sender)
, the tx will revert at require(underlyingValue(msg.sender) >= _amount)
.
As a result, resume()
can not be done, and the debt can't be repaid.
Given:
Expected results: actualValueTransfered = _shortage;
Actual results: actualValueTransfered < _shortage.
The precision loss isn't just happening on special numbers, but will most certainly always revert the txs.
This will malfunction the contract as the index pool can not compensate()
, therefore the pool can not resume()
. Causing the funds of the LPs of the pool and the index pool to be frozen, and other stakeholders of the same vault will suffer fund loss from an unfair share of the funds compensated before.
Change to:
if (totalLiquidity() < _amount) { //Insolvency case _shortage = _amount - _value; uint256 _cds = ICDSTemplate(registry.getCDS(address(this))) .compensate(_shortage); _compensated = vault.underlyingValue(address(this)); } vault.offsetDebt(_compensated, msg.sender);
#0 - takadr
2022-01-24T15:32:24Z
@oishun1112 I think the suggested code doesn't make sense to me because _cds
is not being used and vault.underlyingValue(address(this))
is defined as _value
before.
I guess what he want to do is something like this to avoid precision loss?
if (totalLiquidity() < _amount) { //Insolvency case _shortage = _amount - _value; uint256 _cds = ICDSTemplate(registry.getCDS(address(this))) .compensate(_shortage); if(_cds < _shortage) { _compensated = _value + _cds; } else { // To avoid reversion due to precision loss on "underlyingValue(msg.sender) >= _amount" in "offsetDebt" _compensated = vault.underlyingValue(msg.sender); } } vault.offsetDebt(_compensated, msg.sender);
#1 - oishun1112
2022-01-27T05:34:29Z
he meant, _value + _cds ≒ vault.underlyingValue(address(this)) and this is true.
cds.compensate(_amount)
vault.transferValue(_compensated, msg.sender)
_attributions = (_amount * totalAttributions) / _valueAll
_attributions can be round down and become smaller than _amount's value. => _compensated can be higher than actual attribution transfer.
vault.offsetDebt(_compensated, msg.sender);
try to offsetDebt with _compensated.
require(underlyingValue(msg.sender, _valueAll) >= _amount)
_amount can be higher than actual attribution of index.
return (_valueAll * attributions[_target]) / totalAttributions
=> attributions[index] can be less than intended, so return can be less than intended too. => trigger require() in step4
#2 - oishun1112
2022-01-27T05:38:45Z
In conclusion, this can be reverted due to the rounding down in the process of
value_1(_cds) -> attribution -> value_2 (underlyingValue(index) )
so,
value_1 >= attribution >= value_2
This can be prevent by doing
_compensated = vault.underlyingValue(index)
after step 2, using the actual value index has
#3 - oishun1112
2022-01-27T05:40:03Z
_cds won't be necessary anymore
#4 - takadr
2022-01-27T07:21:25Z
@oishun1112 Ah I got it now thank you for the detail. Yes I was understanding there was vulnerability as he said but I missed that the return value of vault.underlyingValue(index)
is different from the one that is called before cds.compensate(shortage)
🌟 Selected for report: WatchPug
3799.5139 INSURE - $1,329.83
2306.8477 USDC - $2,306.85
WatchPug
function setController(address _controller) public override onlyOwner { require(_controller != address(0), "ERROR_ZERO_ADDRESS"); if (address(controller) != address(0)) { controller.migrate(address(_controller)); controller = IController(_controller); } else { controller = IController(_controller); } emit ControllerSet(_controller); }
The owner of the Vault contract can set an arbitrary address as the controller
.
function utilize() external override returns (uint256 _amount) { if (keeper != address(0)) { require(msg.sender == keeper, "ERROR_NOT_KEEPER"); } _amount = available(); //balance if (_amount > 0) { IERC20(token).safeTransfer(address(controller), _amount); balance -= _amount; controller.earn(address(token), _amount); } }
A malicious controller
contract can transfer funds from the Vault to the attacker.
A malicious/compromised can:
Vault#setController()
and set controller
to a malicious contract;
Vault#utilize()
to deposit all the balance in the Vault contract into the malicious controller contract.Consider disallowing Vault#setController()
to set a new address if a controller is existing, which terminates the possibility of migrating funds to a specified address provided by the owner. Or, putting a timelock to this function at least.
#0 - oishun1112
2022-01-20T07:51:43Z
we assume ownership control is driven safely
#1 - 0xean
2022-01-27T15:22:00Z
Agree with warden that the privilege addresses should not be able to use approvals in a way that rugs users funds.
Based on the fact that we have seen many rug pulls in the space based on compromised "owner" keys, this is a valid attack path.
3 — High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals).
🌟 Selected for report: WatchPug
3799.5139 INSURE - $1,329.83
2306.8477 USDC - $2,306.85
WatchPug
modifier onlyMarket() { require( IRegistry(registry).isListed(msg.sender), "ERROR_ONLY_MARKET" ); _; }
function borrowValue(uint256 _amount, address _to) external onlyMarket override { debts[msg.sender] += _amount; totalDebt += _amount; IERC20(token).safeTransfer(_to, _amount); }
The current design/implementation allows a market address (registered on the registry
) to call Vault#borrowValue()
and transfer tokens to an arbitrary address.
See the PoC section on [WP-H24].
Registry#supportMarket()
.Vault#borrowValue()
.#0 - oishun1112
2022-01-20T07:27:32Z
Ownership has to be stolen to drain funds using this method and we assume ownership control driven safely, so we don't treat this as issue
#1 - 0xean
2022-01-27T15:22:31Z
Agree with warden that the privilege addresses should not be able to use approvals in a way that rugs users funds.
Based on the fact that we have seen many rug pulls in the space based on compromised "owner" keys, this is a valid attack path.
3 — High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals).
1709.7813 INSURE - $598.42
1038.0815 USDC - $1,038.08
WatchPug
Wrong arithmetic.
uint256 _deductionFromIndex = (_debt * _totalCredit * MAGIC_SCALE_1E6) / totalLiquidity(); uint256 _actualDeduction; for (uint256 i = 0; i < indexList.length; i++) { address _index = indexList[i]; uint256 _credit = indicies[_index].credit; if (_credit > 0) { uint256 _shareOfIndex = (_credit * MAGIC_SCALE_1E6) / _totalCredit; uint256 _redeemAmount = _divCeil( _deductionFromIndex, _shareOfIndex ); _actualDeduction += IIndexTemplate(_index).compensate( _redeemAmount ); } }
totalLiquidity = 200,000* 10**18;
totalCredit = 100,000 * 10**18;
debt = 10,000 * 10**18;
[Index Pool 1] Credit = 20,000 * 10**18;
[Index Pool 2] Credit = 30,000 * 10**18;
uint256 _deductionFromIndex = (_debt * _totalCredit * MAGIC_SCALE_1E6) / totalLiquidity(); // _deductionFromIndex = 10,000 * 10**6 * 10**18;
[Index Pool 1]:
uint256 _shareOfIndex = (_credit * MAGIC_SCALE_1E6) / _totalCredit; // _shareOfIndex = 200000 uint256 _redeemAmount = _divCeil( _deductionFromIndex, _shareOfIndex ); // _redeemAmount = 25,000 * 10**18;
[Index Pool 2]:
uint256 _shareOfIndex = (_credit * MAGIC_SCALE_1E6) / _totalCredit; // _shareOfIndex = 300000 uint256 _redeemAmount = _divCeil( _deductionFromIndex, _shareOfIndex ); // _redeemAmount = 16666666666666666666667 (~ 16,666 * 10**18)
In most cases, the transaction will revet on underflow at:
uint256 _shortage = _deductionFromIndex / MAGIC_SCALE_1E6 - _actualDeduction;
In some cases, specific pools will be liable for unfair compensation:
If the CSD is empty, Index Pool 1
only have 6,000 * 10**18
and Index Pool 2
only have 4,000 * 10**18
, the _actualDeduction
will be 10,000 * 10**18
, _deductionFromPool
will be 0
.
Index Pool 1
should only pay 1,000 * 10**18
, but actually paid 6,000 * 10**18
, the LPs of Index Pool 1
now suffer funds loss.
Change to:
uint256 _deductionFromIndex = (_debt * _totalCredit * MAGIC_SCALE_1E6) / totalLiquidity(); uint256 _actualDeduction; for (uint256 i = 0; i < indexList.length; i++) { address _index = indexList[i]; uint256 _credit = indicies[_index].credit; if (_credit > 0) { uint256 _shareOfIndex = (_credit * MAGIC_SCALE_1E6) / _totalCredit; uint256 _redeemAmount = _divCeil( _deductionFromIndex * _shareOfIndex, MAGIC_SCALE_1E6 * MAGIC_SCALE_1E6 ); _actualDeduction += IIndexTemplate(_index).compensate( _redeemAmount ); } }
1709.7813 INSURE - $598.42
1038.0815 USDC - $1,038.08
WatchPug
Based on the context, the system intends to lock all the lps during PayingOut period.
However, the current implementation allows anyone, including LPs to call resume()
and unlock the index pool.
It allows a malicious LP to escape the responsibility for the compensation, at the expense of other LPs paying more than expected.
function resume() external override { uint256 _poolLength = poolList.length; for (uint256 i = 0; i < _poolLength; i++) { require( IPoolTemplate(poolList[i]).paused() == false, "ERROR: POOL_IS_PAUSED" ); } locked = false; emit Resumed(); }
Change to:
function resume() external override { uint256 _poolLength = poolList.length; for (uint256 i = 0; i < _poolLength; i++) { require( IPoolTemplate(poolList[i]).marketStatus() == MarketStatus.Trading, "ERROR: POOL_IS_PAYINGOUT" ); } locked = false; emit Resumed(); }
🌟 Selected for report: WatchPug
3799.5139 INSURE - $1,329.83
2306.8477 USDC - $2,306.85
WatchPug
In the current implementation, when an incident is reported for a certain pool, the index pool can still withdrawCredit()
from the pool, which in the best interest of an index pool, the admin of the index pool is preferred to do so.
This allows the index pool to escape from the responsibility for the risks of invested pools.
Making the LPs of the pool take an unfair share of the responsibility.
totalCredit
= 10,000rewardPerCredit
= 1A
:totalCredit
= 11,000rewardPerCredit
has grown to 1.1
, and applyCover()
has been called, [Index Pool 1] call withdrawCredit()
get 100 premiumtotalCredit
= 10,000pendingEnd
, the pool resume()
,[ Index Pool 1] will not be paying for the compensation since credit
is 0.In our case, [Index Pool 1] earned premium without paying for a part of the compensation.
Change to:
function withdrawCredit(uint256 _credit) external override returns (uint256 _pending) { require( marketStatus == MarketStatus.Trading, "ERROR: WITHDRAW_CREDIT_BAD_CONDITIONS" ); IndexInfo storage _index = indicies[msg.sender];
#0 - oishun1112
2022-01-20T02:52:51Z
to call PoolTemplate: withdrawCredit(), someone has to call IndexTemplate: withdraw(), set(), and adjustAlloc().
set() is onlyOwner, so we assume it's fine() adjustAlloc() is public. this clean up and flatten the credit distribution. withdraw() is public. this reduce totalCredit to distribute. when exceed upperSlack, call adjustAlloc().
#1 - oishun1112
2022-01-20T02:59:12Z
We should lock the credit control when pool is in payout status. This implementation, still allows small amount of withdraw, for users who were requested Withdraw.
#2 - oishun1112
2022-01-20T02:59:25Z
@kohshiba what do you think?
#3 - oishun1112
2022-02-02T09:04:03Z
We have fixed with PVE02 (Peckshield audit) issue together. We will comment the PR link here
🌟 Selected for report: WatchPug
Also found by: Dravee, Ruhum, cmichel, pmerkleplant
149.5717 INSURE - $52.35
90.8114 USDC - $90.81
WatchPug
There are ERC20 tokens that charge fee for every transfer()
/ transferFrom()
.
Vault.sol#addValue()
assumes that the received amount is the same as the transfer amount, and uses it to calculate attributions, balance amounts, etc. While the actual transferred amount can be lower for those tokens.
function addValue( uint256 _amount, address _from, address _beneficiary ) external override onlyMarket returns (uint256 _attributions) { if (totalAttributions == 0) { _attributions = _amount; } else { uint256 _pool = valueAll(); _attributions = (_amount * totalAttributions) / _pool; } IERC20(token).safeTransferFrom(_from, address(this), _amount); balance += _amount; totalAttributions += _attributions; attributions[_beneficiary] += _attributions; }
Consider comparing before and after balance to get the actual transferred amount.
#0 - oishun1112
2022-01-18T08:53:24Z
only USDC can be underwriting asset. We are trying to make it multi currency. We won't implement this now due to the gas consumption, but we will when we develop new type of Vault
#1 - oishun1112
2022-01-19T06:26:50Z
86.5379 INSURE - $30.29
52.5409 USDC - $52.54
WatchPug
function applyCover( uint256 _pending, uint256 _payoutNumerator, uint256 _payoutDenominator, uint256 _incidentTimestamp, bytes32 _merkleRoot, string calldata _rawdata, string calldata _memo ) external override onlyOwner { require(paused == false, "ERROR: UNABLE_TO_APPLY"); incident.payoutNumerator = _payoutNumerator; incident.payoutDenominator = _payoutDenominator; incident.incidentTimestamp = _incidentTimestamp; incident.merkleRoot = _merkleRoot; marketStatus = MarketStatus.Payingout; pendingEnd = block.timestamp + _pending; for (uint256 i = 0; i < indexList.length; i++) { if (indicies[indexList[i]].credit > 0) { IIndexTemplate(indexList[i]).lock(); } } emit CoverApplied( _pending, _payoutNumerator, _payoutDenominator, _incidentTimestamp, _merkleRoot, _rawdata, _memo ); emit MarketStatusChanged(marketStatus); }
An attacker can add a lot of IndexPool to the individual pool, if the length of indexList
is large enough, the gas cost of function applyCover()
can exceed the block limit, making it impossible to applyCover.
Essentially, this allows an attacker (as LP) to get the premium without taking the risk of payout.
Consider allowing applyCover for a certain range of IndexPool.
#0 - oishun1112
2022-01-19T09:41:09Z
170.9781 INSURE - $59.84
103.8081 USDC - $103.81
WatchPug
/** * @notice A liquidity provider supplies collatral to the pool and receives iTokens * @param _amount amount of token to deposit */ function fund(uint256 _amount) external { require(paused == false, "ERROR: PAUSED"); //deposit and pay fees uint256 _attribution = vault.addValue( _amount, msg.sender, address(this) ); surplusPool += _attribution; emit Fund(msg.sender, _amount, _attribution); }
Per the comment above the fund()
function, the liquidity provider should receive iTokens.
A user may get misled by this comment and call the fund()
function and expected to get iTokens in return.
However, in the current implementation, the liquidity provider does not receive iTokens or any other entitlements.
Consider renaming the function to donate()
and remove the misleading comment.
#0 - oishun1112
2022-01-20T08:09:06Z
37.3929 INSURE - $13.09
22.7028 USDC - $22.70
WatchPug
Contracts use assert() instead of require() in multiple places.
Per the Solidity's documentation:
Assert should only be used to test for internal errors, and to check invariants. Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix. Language analysis tools can evaluate your contract to identify the conditions and function calls which will cause a Panic.
if (available() < _amount) { //when USDC in this contract isn't enough uint256 _shortage = _amount - available(); _unutilize(_shortage); assert(available() >= _amount); }
assert(!tokenInitialized); tokenInitialized = true; _name = name_; _symbol = symbol_; _decimals = decimals_;
Change to require()
.
#0 - 0xean
2022-02-22T13:59:36Z
Moving these to dupe of #21
🌟 Selected for report: WatchPug
379.9514 INSURE - $132.98
230.6848 USDC - $230.68
WatchPug
function setController(address _controller) public override onlyOwner { require(_controller != address(0), "ERROR_ZERO_ADDRESS"); if (address(controller) != address(0)) { controller.migrate(address(_controller)); controller = IController(_controller); } else { controller = IController(_controller); } emit ControllerSet(_controller); }
controller.migrate()
is a critical operation, we recommend adding validation for the amount of migrated funds.
Can be changed to:
function setController(address _controller) public override onlyOwner { require(_controller != address(0), "ERROR_ZERO_ADDRESS"); if (address(controller) != address(0)) { uint256 beforeUnderlying = controller.valueAll(); controller.migrate(address(_controller)); require(IController(_controller).valueAll() >= beforeUnderlying, "..."); controller = IController(_controller); } else { controller = IController(_controller); } emit ControllerSet(_controller); }
#0 - 0xHaku
2022-01-22T05:55:10Z
@oishun1112
what error message do we set at require(IController(_controller).valueAll() >= beforeUnderlying, "...");
?
#1 - oishun1112
2022-01-22T06:22:30Z
@taka0409 let's have "ERROR_VALUE_ALL_DECREASED"
🌟 Selected for report: WatchPug
379.9514 INSURE - $132.98
230.6848 USDC - $230.68
WatchPug
function _unutilize(uint256 _amount) internal { require(address(controller) != address(0), "ERROR_CONTROLLER_NOT_SET"); controller.withdraw(address(this), _amount); balance += _amount; }
Can be changed to:
function _unutilize(uint256 _amount) internal { require(address(controller) != address(0), "ERROR_CONTROLLER_NOT_SET"); uint256 beforeBalance = IERC20(token).balanceOf(address(this)); controller.withdraw(address(this), _amount); uint256 received = IERC20(token).balanceOf(address(this)) - beforeBalance; require(received >= _amount, "..."); balance += received; }
#0 - 0xHaku
2022-01-22T05:35:24Z
@oishun1112
what error message do we set at require(received >= _amount, "...");
?
#1 - oishun1112
2022-01-22T06:21:28Z
@taka0409 let's have "ERROR_INSUFFICIENT_RETURN_VALUE"
6.1284 INSURE - $2.14
3.2174 USDC - $3.22
WatchPug
import "hardhat/console.sol";
import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
Remove unused code to reduce gas cost for deployment.
#0 - oishun1112
2022-01-13T11:21:51Z
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
The Wrapped Ether (WETH) ERC-20 contract has a gas optimization that does not update the allowance if it is the max uint.
The latest version of OpenZeppelin's ERC20 token contract also adopted this optimization.
function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; require( currentAllowance >= amount, "ERC20: transfer amount exceeds allowance" ); _approve(sender, _msgSender(), currentAllowance - amount); return true; }
See:
Change to:
function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; if (currentAllowance != type(uint256).max) { require( currentAllowance >= amount, "ERC20: transfer amount exceeds allowance" ); _approve(sender, _msgSender(), currentAllowance - amount); } return true; }
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
if (_supply > 0 && _liquidity > 0) { _mintAmount = (_amount * _supply) / _liquidity; } else if (_supply > 0 && _liquidity == 0) { //when vault lose all underwritten asset = _mintAmount = _amount * _supply; //dilute LP token value af. Start CDS again. } else { //when _supply == 0, _mintAmount = _amount; }
Change to:
if (_supply == 0) { _mintAmount = _amount; } else { _mintAmount = _liquidity == 0 ? _amount * _supply : (_amount * _supply) / _liquidity; }
#0 - oishun1112
2022-01-14T04:03:24Z
if (_supply != 0) { _mintAmount = _liquidity == 0 ? _amount * _supply : (_amount * _supply) / _liquidity; } else { _mintAmount = _amount; }
we'd like to flip, since _supply != 0 will be true for almost every time
#1 - 0xHaku
2022-01-25T16:33:29Z
@oishun1112 already fixed.
#2 - oishun1112
2022-01-27T06:43:24Z
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
string memory _name = "InsureDAO-CDS"; string memory _symbol = "iCDS"; uint8 _decimals = IERC20Metadata(_references[0]).decimals(); initializeToken(_name, _symbol, _decimals);
The local variable _name
, _symbol
, _decimals
is used only once. Making the expression inline can save gas.
Change to:
initializeToken("InsureDAO-CDS", "iCDS", IERC20Metadata(_references[0]).decimals());
Other examples include:
uint256 _balance = balanceOf(msg.sender); require(_balance >= _amount, "ERROR: REQUEST_EXCEED_BALANCE");
uint256 _surplusAttribution = surplusPool;
uint256 _assetReserve = asset.safeBalance(); require(_assetReserve >= assetReserve + assetIn, 'E304');
uint256 _collateralReserve = collateral.safeBalance(); require(_collateralReserve >= collateralReserve + collateralIn, 'E305');
uint256 _shortage; if (totalLiquidity() < _amount) { //Insolvency case _shortage = _amount - _value; uint256 _cds = ICDSTemplate(registry.getCDS(address(this))) .compensate(_shortage); _compensated = _value + _cds; }
_shortage
and _cds
.
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
Removing return 0
can make the code simpler and save some gas.
function rate() external view returns (uint256) { if (totalSupply() > 0) { return (vault.attributionValue(crowdPool) * MAGIC_SCALE_1E6) / totalSupply(); } else { return 0; } }
Can be changed to:
function rate() external view returns (uint256) { if (totalSupply() > 0) { return (vault.attributionValue(crowdPool) * MAGIC_SCALE_1E6) / totalSupply(); } }
Other examples include:
if (_balance == 0) { return 0; } else { return _balance * vault.attributionValue(crowdPool) / totalSupply(); }
Can be changed to:
if (_balance > 0) { return _balance * vault.attributionValue(crowdPool) / totalSupply(); }
for (uint256 i = 0; i < _references.length; i++)
Can be changed to:
for (uint256 i; i < _references.length; i++)
if (totalLiquidity() > 0) { return (totalAllocatedCredit * MAGIC_SCALE_1E6) / totalLiquidity(); } else { return 0; }
Can be changed to:
if (totalLiquidity() > 0) { return (totalAllocatedCredit * MAGIC_SCALE_1E6) / totalLiquidity(); }
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
Check of allowance can be done earlier (before _transfer()
) to save some gas for failure transactions.
function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; require( currentAllowance >= amount, "ERC20: transfer amount exceeds allowance" ); _approve(sender, _msgSender(), currentAllowance - amount); return true; }
See:
#0 - 0xkenta
2022-01-23T02:37:51Z
Fixed with this PR https://github.com/InsureDAO/pool-contracts/pull/145.
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
When there are multiple checks, adjusting the sequence to allow the tx to fail earlier can save some gas.
Checks using less gas should be executed earlier than those with higher gas costs, to avoid unnecessary storage read, arithmetic operations, etc when it reverts.
For example:
require(paused == false, "ERROR: PAUSED"); require( request.timestamp + parameters.getLockup(msg.sender) < block.timestamp, "ERROR: WITHDRAWAL_QUEUE" ); require( request.timestamp + parameters.getLockup(msg.sender) + parameters.getWithdrawable(msg.sender) > block.timestamp, "ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST" ); require( request.amount >= _amount, "ERROR: WITHDRAWAL_EXCEEDED_REQUEST" ); require(_amount > 0, "ERROR: WITHDRAWAL_ZERO");
The check of _amount > 0
can be done earlier to avoid reading from storage when _amount = 0
.
Change to:
require(paused == false, "ERROR: PAUSED"); require(_amount > 0, "ERROR: WITHDRAWAL_ZERO"); require( request.amount >= _amount, "ERROR: WITHDRAWAL_EXCEEDED_REQUEST" ); require( request.timestamp + parameters.getLockup(msg.sender) < block.timestamp, "ERROR: WITHDRAWAL_QUEUE" ); require( request.timestamp + parameters.getLockup(msg.sender) + parameters.getWithdrawable(msg.sender) > block.timestamp, "ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST" );
Other examples include:
uint256 _balance = balanceOf(msg.sender); require(_balance >= _amount, "ERROR: REQUEST_EXCEED_BALANCE"); require(_amount > 0, "ERROR: REQUEST_ZERO");
require(locked == false, "ERROR: WITHDRAWAL_PENDING"); require( _requestTime + _lockup < block.timestamp, "ERROR: WITHDRAWAL_QUEUE" ); require( _requestTime + _lockup + parameters.getWithdrawable(msg.sender) > block.timestamp, "ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST" ); require( withdrawalReq[msg.sender].amount >= _amount, "ERROR: WITHDRAWAL_EXCEEDED_REQUEST" ); require(_amount > 0, "ERROR: WITHDRAWAL_ZERO"); require( _retVal <= withdrawable(), "ERROR: WITHDRAW_INSUFFICIENT_LIQUIDITY" );
28.022 INSURE - $9.81
14.7115 USDC - $14.71
WatchPug
Cache and reusing the function call results, instead of calling it again, can save gas from unnecessary code execution.
if (available() < _amount) { //when USDC in this contract isn't enough uint256 _shortage = _amount - available(); _unutilize(_shortage); assert(available() >= _amount); } balance -= _amount; IERC20(token).safeTransfer(_to, _amount);
Change to:
uint256 availableAmount = available() if ( availableAmount < _amount) { //when USDC in this contract isn't enough uint256 _shortage = _amount - available(); _unutilize(_shortage); assert(availableAmount >= _amount); } balance -= _amount; IERC20(token).safeTransfer(_to, _amount);
Other examples include:
function rate() external view returns (uint256) { if (totalSupply() > 0) { return (vault.attributionValue(crowdPool) * MAGIC_SCALE_1E6) / totalSupply(); } else { return 0; } }
totalSupply()
if (available() < _retVal) { uint256 _shortage = _retVal - available(); _unutilize(_shortage); }
available()
#0 - blue32captain
2022-01-20T05:21:58Z
https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L163-L173 available() in require could not be changed since its value changes in above function.
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
if (_available >= _amount) { _compensated = _amount; _attributionLoss = vault.transferValue(_amount, msg.sender); emit Compensated(msg.sender, _amount); } else { //when CDS cannot afford, pay as much as possible _compensated = _available; _attributionLoss = vault.transferValue(_available, msg.sender); emit Compensated(msg.sender, _available); }
Change to:
_compensated = _available >= _amount? _amount: _available; _attributionLoss = vault.transferValue(_compensated, msg.sender); emit Compensated(msg.sender, _compensated);
#0 - 0xHaku
2022-01-25T16:18:50Z
@oishun1112 already fixed.
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
Check marketStatus
before for loops can save gas from unnecessary repeated checks.
function unlockBatch(uint256[] calldata _ids) external { for (uint256 i = 0; i < _ids.length; i++) { unlock(_ids[i]); } } function unlock(uint256 _id) public { require( insurances[_id].status == true && marketStatus == MarketStatus.Trading && insurances[_id].endTime + parameters.getGrace(msg.sender) < block.timestamp, "ERROR: UNLOCK_BAD_COINDITIONS" ); insurances[_id].status == false; lockedAmount = lockedAmount - insurances[_id].amount; emit Unlocked(_id, insurances[_id].amount); }
Change to:
function unlockBatch(uint256[] calldata _ids) external { require(marketStatus == MarketStatus.Trading, "ERROR: UNLOCK_BAD_COINDITIONS") for (uint256 i = 0; i < _ids.length; i++) { _unlock(_ids[i]); } } function unlock(uint256 _id) external { require(marketStatus == MarketStatus.Trading, "ERROR: UNLOCK_BAD_COINDITIONS"); _unlock(_id); } function _unlock(uint256 _id) internal { require( insurances[_id].status == true && insurances[_id].endTime + parameters.getGrace(msg.sender) < block.timestamp, "ERROR: UNLOCK_BAD_COINDITIONS" ); insurances[_id].status == false; lockedAmount = lockedAmount - insurances[_id].amount; emit Unlocked(_id, insurances[_id].amount); }
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled.
bool public paused;
Consider changing to:
uint256 private constant _NOT_PAUSED = 0; uint256 private constant _PAUSED = 1; uint256 public paused;
Other examples include:
bool public initialized;
bool public initialized; bool public paused; bool public locked;
#0 - oishun1112
2022-01-16T06:52:19Z
I did experiment and could reduce gas around 110. but keep the original code for the readability.
🌟 Selected for report: Jujic
Also found by: TomFrenchBlockchain, WatchPug
16.8132 INSURE - $5.88
8.8269 USDC - $8.83
WatchPug
Insurance storage _insurance = insurances[_id]; require(_insurance.status == true, "ERROR: INSURANCE_NOT_ACTIVE"); uint256 _payoutNumerator = incident.payoutNumerator; uint256 _payoutDenominator = incident.payoutDenominator; uint256 _incidentTimestamp = incident.incidentTimestamp; bytes32 _targets = incident.merkleRoot;
L549 Insurance storage insurance = insurances[_id];
can be changed to Insurance memory insurance = insurances[_id];
.
And at L583 _insurance.status = false;
should be changed to insurances[_id].status = false;
.
struct Insurance { uint256 id; //each insuance has their own id uint256 startTime; //timestamp of starttime uint256 endTime; //timestamp of endtime uint256 amount; //insured amount bytes32 target; //target id in bytes32 address insured; //the address holds the right to get insured bool status; //true if insurance is not expired or redeemed }
Because insured
and status
are packed in the same slot, copying to memory instead of reading from storage separately can save one SLOAD
.
#0 - 0xean
2022-01-28T02:36:58Z
dupe of #253
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
Every call to an external contract costs a decent amount of gas. For optimization of gas usage, external call results should be cached if they are being used for more than one time.
For example:
require( attributions[msg.sender] > 0 && underlyingValue(msg.sender) >= _amount, "ERROR_WITHDRAW-VALUE_BADCONDITOONS" ); _attributions = (totalAttributions * _amount) / valueAll();
In Vault#withdrawValue()
, controller.valueAll()
is called twice:
underlyingValue(msg.sender)
-> valueAll()
-> controller.valueAll()
;valueAll()
-> controller.valueAll()
.function underlyingValue(address _target) public view override returns (uint256) { if (attributions[_target] > 0) { return (valueAll() * attributions[_target]) / totalAttributions; } else { return 0; } }
function valueAll() public view returns (uint256) { if (address(controller) != address(0)) { return balance + controller.valueAll(); } else { return balance; } }
#0 - oishun1112
2022-01-26T08:43:46Z
created overload function of underlyingValue(). Some functions that can cache valueAll() pass the cached valueAll into the overloaded underlyingValue()
🌟 Selected for report: WatchPug
62.2711 INSURE - $21.79
32.6923 USDC - $32.69
WatchPug
constructor() { _owner = msg.sender; emit AcceptNewOwnership(_owner); }
At L19, the parameter of AcceptNewOwnership
can use msg.sender
directly to avoid unnecessary storage read of _owner
to save some gas.
function acceptTransferOwnership() external override onlyFutureOwner { /*** *@notice Accept a transfer of ownership */ _owner = _futureOwner; emit AcceptNewOwnership(_owner); }
At L69, _futureOwner
can use msg.sender
directly to avoid unnecessary storage read of _futureOwner
to save some gas.
As onlyFutureOwner()
ensures that require(_futureOwner == msg.sender, "...");
.
16.8132 INSURE - $5.88
8.8269 USDC - $8.83
WatchPug
uint256 public constant MAGIC_SCALE_1E6 = 1e6; //internal multiplication scale 1e6 to reduce decimal truncation
uint256 public constant MAGIC_SCALE_1E6 = 1e6; //internal multiplication scale 1e6 to reduce decimal truncation
uint256 public constant MAGIC_SCALE_1E6 = 1e6; //internal multiplication scale 1e6 to reduce decimal truncation
uint256 public constant MAGIC_SCALE_1E6 = 1e6; //internal multiplication scale 1e6 to reduce decimal truncation
For the constants that should not be public
, changing them to private
/ internal
can save some gas. To avoid unnecessary getter functions.
2.9784 INSURE - $1.04
1.5637 USDC - $1.56
WatchPug
It is cheaper to use != 0
than > 0
for uint256.
require(_amount > 0, "ERROR: DEPOSIT_ZERO");
function rate() external view returns (uint256) { if (totalSupply() > 0) { return (vault.attributionValue(crowdPool) * MAGIC_SCALE_1E6) / totalSupply(); } else { return 0; } }
require(_amount > 0, "ERROR: DEPOSIT_ZERO");
if (_supply > 0 && _totalLiquidity > 0) {
require(_amount > 0, "ERROR: DEPOSIT_ZERO");
#0 - oishun1112
2022-01-13T17:00:38Z
#1 - 0xean
2022-01-28T00:08:12Z
dupe of #36
2.4125 INSURE - $0.84
1.2666 USDC - $1.27
WatchPug
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Instances include:
Factory.sol#createMarket()
Can be changed to:
uint256 _referencesLength = _references.length; if (_referencesLength > 0) { for (uint256 i = 0; i < _referencesLength; i++) { require( reflist[address(_template)][i][_references[i]] == true || reflist[address(_template)][i][address(0)] == true, "ERROR: UNAUTHORIZED_REFERENCE" ); } }
#0 - oishun1112
2022-01-13T11:33:26Z
6.1284 INSURE - $2.14
3.2174 USDC - $3.22
WatchPug
For the arithmetic operations that will never over/underflow, using the unchecked directive (Solidity v0.8 has default overflow/underflow checks) can save some gas from the unnecessary internal over/underflow checks.
For example:
if (available() < _amount) { //when USDC in this contract isn't enough uint256 _shortage = _amount - available(); _unutilize(_shortage); assert(available() >= _amount); }
_amount - available()
will never underflow.
if (_allocated > _availableBalance) { uint256 _availableRate = (_availableBalance * MAGIC_SCALE_1E6) / _allocated; uint256 _lockedCredit = _allocated - _availableBalance; if (i == 0 || _availableRate < _lowestAvailableRate) { _lowestAvailableRate = _availableRate; _targetLockedCreditScore = _lockedCredit; _targetAllocPoint = _allocPoint; }
_allocated - _availableBalance
will never underflow.
require( attributions[msg.sender] >= _attribution, "ERROR_WITHDRAW-ATTRIBUTION_BADCONDITOONS" ); _retVal = (_attribution * valueAll()) / totalAttributions; attributions[msg.sender] -= _attribution; totalAttributions -= _attribution;
attributions[msg.sender] -= _attribution
will never underflow.
#0 - oishun1112
2022-01-16T07:34:29Z
two of three are duplicated with https://github.com/code-423n4/2022-01-insure-findings/issues/66
#1 - oishun1112
2022-01-16T07:35:09Z
#2 - 0xean
2022-01-28T00:19:11Z
dupe of #66
2.9784 INSURE - $1.04
1.5637 USDC - $1.56
WatchPug
Using ++i
is more gas efficient than i++
, especially in a loop.
We suggest to use unchecked {++i} to even better gas performance. (for >= 0.8)
For example:
for (uint256 i = 0; i < _references.length; i++)
for (uint256 i = 0; i < _length; i++)
for (uint256 i = 0; i < _length; i++)
for (uint256 i = 0; i < _poolLength; i++)
#0 - oishun1112
2022-01-13T16:58:56Z