Platform: Code4rena
Start Date: 16/11/2021
Pot Size: $50,000 ETH
Total HM: 11
Participants: 17
Period: 7 days
Judge: LSDan
Total Solo HM: 8
Id: 49
League: ETH
Rank: 3/17
Findings: 5
Award: $6,558.33
🌟 Selected for report: 9
🚀 Solo Findings: 2
🌟 Selected for report: gpersoon
0.4973 ETH - $2,297.86
gpersoon
The contract LogExpMath.sol seems to be a fork of the balancer LogExpMath.sol contract. It is mostly similar, except for checks for x and y being 0 in the beginning of the function pow(), see below.
This omission might lead to unexpected results.
function pow(uint256 x, uint256 y) internal pure returns (uint256) { unchecked { _require(x < 2**255, Errors.X_OUT_OF_BOUNDS);
function pow(uint256 x, uint256 y) internal pure returns (uint256) { if (y == 0) { // We solve the 0^0 indetermination by making it equal one. return uint256(ONE_18); } if (x == 0) { return 0; } _require(x < 2**255, Errors.X_OUT_OF_BOUNDS);
Check if the extra code of the balance contract is useful and if so add it.
#0 - commercium-sys
2021-12-07T21:35:09Z
Out of scope
#1 - dmvt
2021-12-18T13:17:46Z
I disagree with sponsor regarding scope. The Contracts section of the Contest Scope lists several contracts which rely on contracts/libraries/FixedPoint.sol
. This contract uses the pow
function containing the issue described. The warden has not described an exact attack but has show a math issue, which can certainly lead to a hypothetical loss of funds. Medium severity is appropriate and sponsor should definitely fix this.
🌟 Selected for report: gpersoon
0.4973 ETH - $2,297.86
gpersoon
The function disableCollateral of OverlayV1Mothership.sol doesn't set collateralActive[_collateral] = false; But it does revoke the roles.
Now enableCollateral can never be used because collateralActive[_collateral] ==true and it will never pass the second require. So you can never grant the roles again.
Note: enableCollateral also doesn't set collateralActive[_collateral] = true
function enableCollateral (address _collateral) external onlyGovernor { require(collateralExists[_collateral], "OVLV1:!exists"); require(!collateralActive[_collateral], "OVLV1:!disabled"); OverlayToken(ovl).grantRole(OverlayToken(ovl).MINTER_ROLE(), _collateral); OverlayToken(ovl).grantRole(OverlayToken(ovl).BURNER_ROLE(), _collateral); } function disableCollateral (address _collateral) external onlyGovernor { require(collateralActive[_collateral], "OVLV1:!enabled"); OverlayToken(ovl).revokeRole(OverlayToken(ovl).MINTER_ROLE(), _collateral); OverlayToken(ovl).revokeRole(OverlayToken(ovl).BURNER_ROLE(), _collateral); }
In function enableCollateral() add the following (after the require): collateralActive[_collateral] = true;
In function disableCollateral add the following (after the require): collateralActive[_collateral] = false;
0.0378 ETH - $174.45
gpersoon
The functions _transferMint() and _transferBurn() of OverlayToken.sol don't update _totalSupply. Whereas the similar functions _mint() and _burn() do update _totalSupply.
This means that _totalSupply and totalSupply() will not show a realistic view of the total OVL tokens.
For the protocol itself it isn't such a problem because this value isn't used in the protocol (as far as I can see). But other protocols building on Overlay may use it, as well as user interfaces and analytic platforms.
function _mint( address account, uint256 amount) internal virtual { ... _totalSupply += amount; https://github.com/code-423n4/2021-11-overlay/blob/914bed22f190ebe7088194453bab08c424c3f70c/contracts/ovl/OverlayToken.sol#L376-L395 ```JS function _burn(address account, uint256 amount) internal virtual { ... _totalSupply -= amount; https://github.com/code-423n4/2021-11-overlay/blob/914bed22f190ebe7088194453bab08c424c3f70c/contracts/ovl/OverlayToken.sol#L194-L212 https://github.com/code-423n4/2021-11-overlay/blob/914bed22f190ebe7088194453bab08c424c3f70c/contracts/ovl/OverlayToken.sol#L268-L286 ## Tools Used ## Recommended Mitigation Steps Update _totalSupply in _transferMint() and _transferBurn()
#0 - commercium-sys
2021-12-07T21:49:07Z
We're not sure if this is a 1 or a 2. Definitely, at least a one - this is an incorrect implementation of the spec.
But is it a two? It wouldn't lose funds with our contracts, we make no use of the total supply of OVL in our accounting.
This might prove to be a vulnerability if another protocol, like Ribbon, used us for a vault of theirs, made use of total supply, and failed to discern this problem.
🌟 Selected for report: gpersoon
0.1658 ETH - $765.95
gpersoon
The contract OverlayV1Market is abstract and includes several other contracts that are also abstract. The only deviation is the contract OverlayV1OI which isn't abstract.
The risk is that OverlayV1OI could be accidentally deployed separately.
abstract contract OverlayV1Market is OverlayV1Governance {
abstract contract OverlayV1Governance is OverlayV1Comptroller, OverlayV1OI, OverlayV1PricePoint {
abstract contract OverlayV1Comptroller {
contract OverlayV1OI { // only contract that isn't abstract
abstract contract OverlayV1PricePoint {
Make contract OverlayV1OI also abstract
🌟 Selected for report: gpersoon
gpersoon
Suppose you try to build a position and have set the leverage accidentally to 0 (which can be done if you call the smart contract directly). Then the function build() will call enterOI() which will revert when trying to calculate debtAdjusted .
However no user friendly error message is given.
function enterOI ( bool _isLong, uint _collateral, uint _leverage ) external onlyCollateral returns (...) { ... collateralAdjusted_ = _collateral - _impact - fee_; // will be > 0 oiAdjusted_ = collateralAdjusted_ * _leverage; // if _leverage==0 then oiAdjusted_ == 0 debtAdjusted_ = oiAdjusted_ - collateralAdjusted_; // will be negative and thus will revert
Add something like the following to the function build() require(_leverage != 0, "OVLV1:leverage==0")
0.0072 ETH - $33.42
gpersoon
The function roll() of OverlayV1Comptroller.sol can be simplified. This saves some gas and also makes the function easier to read. See below at "Recommended Mitigation Steps"
function roll( Roller[60] storage rollers, Roller memory _roller, uint _lastMoment, uint _cycloid ) internal returns ( uint cycloid_) { if (_roller.time != _lastMoment) { _cycloid += 1; if (_cycloid < CHORD) { rollers[_cycloid] = _roller; } else { _cycloid = 0; rollers[_cycloid] = _roller; } } else { rollers[_cycloid] = _roller; } cycloid_ = _cycloid; }
Change the function to:
function roll (Roller[60] storage rollers,Roller memory _roller,uint _lastMoment,uint _cycloid) internal returns (uint cycloid_) { if (_roller.time != _lastMoment) _cycloid = (_cycloid + 1) % CHORD; rollers[_cycloid] = _roller; cycloid_ = _cycloid; }
🌟 Selected for report: gpersoon
0.0161 ETH - $74.28
gpersoon
The function oiCap() of OverlayV1Comptroller.sol save the value of brrrrdExpected in a tmp variable _brrrrdExpected. Lateron brrrrdExpected is still used while _brrrrdExpected could also be used. This saves a bit of gas.
function oiCap() public virtual view returns ( uint cap_ ) { ... uint _brrrrdExpected = brrrrdExpected; ... cap_ = _surpassed ? 0 : _burnt || _expected ? _oiCap(false, depth(), staticCap, 0, 0) : _oiCap(true, depth(), staticCap, _brrrrd, brrrrdExpected); // can also use _brrrrdExpected
Replace
: _oiCap(true, depth(), staticCap, _brrrrd, brrrrdExpected);
with
: _oiCap(true, depth(), staticCap, _brrrrd, _brrrrdExpected);
🌟 Selected for report: gpersoon
0.0161 ETH - $74.28
gpersoon
The function value() of OverlayV1OVLCollateral.sol doesn't explicitly check for liquidated positions. However because oiShares and debt are set to 0 during liquidation the resulting value will still be 0.
It seems more logical to check for liquidation in the beginning of the function and immediately return 0. This saves gas for the situation where the function value() is called from another smart contract.
function value ( uint _positionId ) public view returns ( uint256 value_) { Position.Info storage pos = positions[_positionId]; IOverlayV1Market _market = IOverlayV1Market(pos.market); ( uint _oi, uint _oiShares, uint _priceFrame ) = _market.positionInfo( pos.isLong, pos.pricePoint ); value_ = pos.value( _oi, _oiShares, _priceFrame ); }
Add something like the following to function value():
if (pos.oiShares == 0) return 0; // liquidated
🌟 Selected for report: gpersoon
0.0161 ETH - $74.28
gpersoon
In the function unwind() of OverlayV1OVLCollateral.sol a tmp variable _userOiShares is used the store the value of _shares. However _shares is still uses multiple times in the function.
Using _userOiShares everywhere would save gas.
function unwind ( uint256 _positionId, uint256 _shares ) external { require( 0 < _shares && _shares <= balanceOf(msg.sender, _positionId), "OVLV1:!shares"); // uses _shares ... uint _userOiShares = _shares; // move to start of the function uint _userNotional = _shares * pos.notional(_oi, _oiShares, _priceFrame) / _totalPosShares; // uses _shares uint _userDebt = _shares * pos.debt / _totalPosShares; // uses _shares uint _userCost = _shares * pos.cost / _totalPosShares; // uses _shares uint _userOi = _shares * pos.oi(_oi, _oiShares) / _totalPosShares; // uses _shares ... _burn(msg.sender, _positionId, _shares); // uses _shares
Move "uint _userOiShares = _shares;" to the start of function unwind() Replace all other instances of "_shares" with "_userOiShares"