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: 8/17
Findings: 4
Award: $2,458.72
🌟 Selected for report: 11
🚀 Solo Findings: 0
WatchPug
function _transferBurn( address sender, address recipient, uint256 amount, uint256 burnt ) internal { uint256 senderBalance = _balances[sender]; require(senderBalance >= amount + burnt, "OVL:balance<amount+burnt"); unchecked { _balances[sender] = senderBalance - amount - burnt; } _balances[recipient] += amount; emit Transfer(sender, recipient, amount); emit Transfer(sender, address(0), burnt); }
Per the comment, _transferBurn
is supposed to be a combination of transfer()
and burn()
.
However, in the current implementation, _transferBurn
won't reduce the totalSupply
, which is an essential part of burn()
.
Change to:
function _transferBurn( address sender, address recipient, uint256 amount, uint256 burnt ) internal { uint256 senderBalance = _balances[sender]; require(senderBalance >= amount + burnt, "OVL:balance<amount+burnt"); unchecked { _balances[sender] = senderBalance - amount - burnt; } _balances[recipient] += amount; _totalSupply -= amount; emit Transfer(sender, recipient, amount); emit Transfer(sender, address(0), burnt); }
#0 - mikeyrf
2021-12-06T23:45:40Z
duplicate #59
WatchPug
Based on the context, FEE
and MARGIN_MAINTENANCE
rates should be bounded. However, the current implementation does not enforce these bounds.
uint16 public constant MIN_FEE = 1; // 0.01% uint16 public constant MAX_FEE = 100; // 1.00% uint16 public constant MIN_MARGIN_MAINTENANCE = 100; // 1% maintenance uint16 public constant MAX_MARGIN_MAINTENANCE = 6000; // 60% maintenance
Change to:
function adjustGlobalParams( uint16 _fee, uint16 _feeBurnRate, address _feeTo ) external onlyGovernor { require(_fee >= MIN_FEE, "OVLV1:MIN_FEE"); require(_fee <= MAX_FEE, "OVLV1:MAX_FEE"); fee = _fee; feeBurnRate = _feeBurnRate; feeTo = _feeTo; }
function setMarketInfo ( address _market, uint _marginMaintenance, uint _marginRewardRate, uint _maxLeverage ) external onlyGovernor { require(_marginMaintenance >= mothership.MIN_MARGIN_MAINTENANCE(), "OVLV1:MIN_MARGIN_MAINTENANCE"); require(_marginMaintenance <= mothership.MAX_MARGIN_MAINTENANCE(), "OVLV1:MAX_MARGIN_MAINTENANCE"); marketInfo[_market].marginMaintenance = _marginMaintenance; marketInfo[_market].marginRewardRate = _marginRewardRate; marketInfo[_market].maxLeverage = _maxLeverage; }
#0 - mikeyrf
2021-12-06T23:28:34Z
duplicate #77 - bounds on governance params
🌟 Selected for report: WatchPug
0.1658 ETH - $765.95
WatchPug
OverlayToken.sol#_transferBurn()
and _transferMint()
does not validate recipient != address(0)
. While in _transfer()
, it validates the input and make sure recipient != address(0)
.
function _transferMint( address sender, address recipient, uint256 amount, uint256 minted ) internal { uint256 senderBalance = _balances[sender]; require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); unchecked { _balances[sender] = senderBalance - amount; } _balances[recipient] += amount + minted; emit Transfer(sender, recipient, amount); emit Transfer(address(0), recipient, minted); }
function _transferBurn( address sender, address recipient, uint256 amount, uint256 burnt ) internal { uint256 senderBalance = _balances[sender]; require(senderBalance >= amount + burnt, "OVL:balance<amount+burnt"); unchecked { _balances[sender] = senderBalance - amount - burnt; } _balances[recipient] += amount; emit Transfer(sender, recipient, amount); emit Transfer(sender, address(0), burnt); }
Consider adding input validations.
🌟 Selected for report: WatchPug
WatchPug
Based on the context, marginBurnRate
should be able to be updated after deployment. However, there is no function to update it.
Change to:
function adjustGlobalParams( uint16 _fee, uint16 _feeBurnRate, address _feeTo, uint _marginBurnRate ) external onlyGovernor { fee = _fee; feeBurnRate = _feeBurnRate; feeTo = _feeTo; marginBurnRate = _marginBurnRate; }
Or change marginBurnRate
to immutable if it's not supposed to be updated later (for gas saving).
WatchPug
Unused events increase contract size and gas usage at deployment.
event log(string k , uint v);
log
is unused.
#0 - mikeyrf
2021-12-07T00:30:49Z
duplicate #122 - dead code to be removed
🌟 Selected for report: WatchPug
Also found by: defsec, harleythedog, ye0lde
0.0029 ETH - $13.54
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 (_userValueAdjusted > _userDebt) _userValueAdjusted -= _userDebt;
_userValueAdjusted -= _userDebt
will never underflow.
IOverlayV1Market(pos.market).exitOI( pos.isLong, _userOi, _userOiShares, _userCost < _userValueAdjusted ? _userValueAdjusted - _userCost : 0, _userCost < _userValueAdjusted ? 0 : _userCost - _userValueAdjusted );
_userValueAdjusted - _userCost
and _userCost - _userValueAdjusted
will never underflow.
if (_userCost < _userValueAdjusted) { ovl.transferMint( msg.sender, _userCost, _userValueAdjusted - _userCost ); } else { ovl.transferBurn( msg.sender, _userValueAdjusted, _userCost - _userValueAdjusted ); }
_userValueAdjusted - _userCost
and _userCost - _userValueAdjusted
will never underflow.
if (_userValueAdjusted > _userDebt) _userValueAdjusted -= _userDebt;
_userValueAdjusted -= _userDebt
will never underflow.
🌟 Selected for report: WatchPug
0.0161 ETH - $74.28
WatchPug
Redundant code increase contract size and gas usage at deployment.
bytes32 public constant ADMIN_ROLE = 0x00;
constructor() { _setupRole(ADMIN_ROLE, msg.sender); _setupRole(MINTER_ROLE, msg.sender); _setupRole(BURNER_ROLE, msg.sender); _setRoleAdmin(MINTER_ROLE, ADMIN_ROLE); _setRoleAdmin(BURNER_ROLE, ADMIN_ROLE); }
L17, the constant ADMIN_ROLE
is redundant as DEFAULT_ADMIN_ROLE
is already defined in AccessControl
.
L29 can be changed to _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
L32-33 are redundant as the role admin is default to 0x00
therefore, they can be removed.
#0 - commercium-sys
2021-12-07T21:50:08Z
Very minimal gas
🌟 Selected for report: WatchPug
0.0161 ETH - $74.28
WatchPug
ovl.burn(address(this), _feeBurn + _liqBurn); ovl.transfer(_feeTo, _feeForward + _liqForward);
Use transferBurn
can combine two external calls into one to save gas.
0.0043 ETH - $20.05
WatchPug
Every reason string takes at least 32 bytes.
Use short reason strings that fits in 32 bytes or it will become more expensive.
Instances include:
require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
require(sender != address(0), "ERC20: transfer from the zero address"); require(recipient != address(0), "ERC20: transfer to the zero address"); _beforeTokenTransfer(sender, recipient, amount); uint256 senderBalance = _balances[sender]; require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
require(account != address(0), "ERC20: burn from the zero address"); _beforeTokenTransfer(account, address(0), amount); uint256 accountBalance = _balances[account]; require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
require(owner != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address");
#0 - mikeyrf
2021-12-07T21:57:39Z
sponsor acknowledged reason - minimal gas savings
🌟 Selected for report: WatchPug
0.0161 ETH - $74.28
WatchPug
Check of allowance can be done earlier to save some gas for failure transactions.
function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns ( bool success_ ) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance"); unchecked { _approve(sender, _msgSender(), currentAllowance - amount); } success_ = true; }
function transferFromBurn( address sender, address recipient, uint256 amount, uint256 burnt ) public override onlyBurner returns ( bool success ) { _transferBurn(sender, recipient, amount, burnt); uint256 currentAllowance = _allowances[sender][msg.sender]; require(currentAllowance >= amount + burnt, "OVL:allowance<amount+burnt"); unchecked { _approve(sender, msg.sender, currentAllowance - amount - burnt); } success = true; }
function transferFromMint( address sender, address recipient, uint256 amount, uint256 minted ) public override onlyMinter returns ( bool ) { _transferMint(sender, recipient, amount, minted); uint256 currentAllowance = _allowances[sender][msg.sender]; require(currentAllowance >= amount, "OVL:allowance<amount"); unchecked { _approve(sender, msg.sender, currentAllowance - amount); } return true; }
🌟 Selected for report: WatchPug
0.0161 ETH - $74.28
WatchPug
Every call to an external contract costs a decent amount of gas. In OverlayV1OVLCollateral.sol
, mothership.fee()
can be cached as a storage variable and save ~21000 gas each time.
uint _feeAmount = _userNotional.mulUp(mothership.fee());
OverlayV1OVLCollateral.sol
;updateFee()
updateFee()
after OverlayV1Mothership.sol#adjustGlobalParams()
0.0072 ETH - $33.42
WatchPug
uint256 internal X96 = 0x1000000000000000000000000;
Some storage variables include X96
will not never be changed and they should not be.
Changing them to constant
can save gas.
🌟 Selected for report: WatchPug
0.0161 ETH - $74.28
WatchPug
modifier lock() { require(unlocked == 1, "OVLV1:!unlocked"); unlocked = 0; _; unlocked = 1; }
SSTORE
from 0 to 1 (or any non-zero value), the cost is 20000;
SSTORE
from 1 to 2 (or any other non-zero value), the cost is 5000.
By storing the original value once again, a refund is triggered (https://eips.ethereum.org/EIPS/eip-2200).
Since refunds are capped to a percentage of the total transaction's gas, it is best to keep them low, to increase the likelihood of the full refund coming into effect.
Therefore, switching between 1, 2 instead of 0, 1 will be more gas efficient.
🌟 Selected for report: WatchPug
0.0161 ETH - $74.28
WatchPug
For the storage variables that will be accessed multiple times, cache them in the stack can save ~100 gas from each extra read (SLOAD
after Berlin).
For example:
ovl
in OverlayV1Mothership#enableCollateral()
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); }
ovl
in OverlayV1Mothership#disableCollateral()
ovl
in OverlayV1Mothership#initializeCollateral()