Platform: Code4rena
Start Date: 21/04/2022
Pot Size: $100,000 USDC
Total HM: 18
Participants: 60
Period: 7 days
Judge: gzeon
Total Solo HM: 10
Id: 112
League: ETH
Rank: 13/60
Findings: 4
Award: $1,144.85
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Dravee
Also found by: IllIllI, MaratCerby, UnusualTurtle, WatchPug, antonttc, berndartmueller, cccz, danb, horsefacts, hyh, pauliax, rayn, wuwe1
Originally submitted by warden horsefacts in https://github.com/code-423n4/2022-04-backd-findings/issues/199, duplicate of https://github.com/code-423n4/2022-04-backd-findings/issues/52.
payable.transfer
EthPool
and EthVault
both use payable(address).transfer
to transfer ETH.
It's considered a best practice to avoid this pattern for ETH transfers, since it forwards a fixed amount of gas and may revert if future gas costs change. (See the Consensys Diligence article here).
function _doTransferOut(address payable to, uint256 amount) internal override { to.transfer(amount); }
function _transfer(address to, uint256 amount) internal override { payable(to).transfer(amount); }
function _depositToTreasury(uint256 amount) internal override { payable(addressProvider.getTreasury()).transfer(amount); }
Consider using OpenZeppelin Address.sendValue
, but take care to avoid reentrancy. Callers of these internal functions should be protected with a reentrancy guard.
#0 - JeeberC4
2022-05-13T19:26:16Z
created required json file
🌟 Selected for report: cccz
Also found by: 0x1f8b, 0xDjango, 0xkatana, Dravee, IllIllI, WatchPug, berndartmueller, defsec, horsefacts, hyh, kenta, rayn, reassor, sorrynotsorry
Originally submitted by warden horsefacts in https://github.com/code-423n4/2022-04-backd-findings/issues/199, duplicate of https://github.com/code-423n4/2022-04-backd-findings/issues/17.
The ChainlinkUsdWrapper#_ethPrice()
function does not check for a nonzero answer or validate that the price was returned in a recent round:
function _ethPrice() private view returns (int256) { (, int256 answer, , , ) = _ethOracle.latestRoundData(); return answer; }
Although callers of ChainlinkUsd#latestRoundData
can check for a nonzero price, they can't verify that the ETH oracle price used in this conversion was returned in a recent round. If the ETH oracle returns a stale price, the wrapper may return an inaccurate conversion.
Recommendation: Validate returned ETH price using roundId
and answeredInRound
.
#0 - JeeberC4
2022-05-13T19:25:56Z
Created required json file
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x52, 0xDjango, 0xkatana, Dravee, Funen, Kenshin, Ruhum, StyxRave, Tadashi, TerrierLover, TrungOre, antonttc, berndartmueller, catchup, csanuragjain, defsec, dipp, fatherOfBlocks, hake, horsefacts, hubble, jayjonah8, joestakey, kebabsec, kenta, m4rio_eth, oyc_109, pauliax, peritoflores, rayn, remora, robee, securerodd, simon135, sorrynotsorry, sseefried, z3s
930.9353 USDC - $930.94
The ChainlinkUsdWrapper#_ethPrice()
function does not check for a nonzero answer or validate that the price was returned in a recent round:
function _ethPrice() private view returns (int256) { (, int256 answer, , , ) = _ethOracle.latestRoundData(); return answer; }
Although callers of ChainlinkUsd#latestRoundData
can check for a nonzero price, they can't verify that the ETH oracle price used in this conversion was returned in a recent round. If the ETH oracle returns a stale price, the wrapper may return an inaccurate conversion.
Recommendation: Validate returned ETH price using roundId
and answeredInRound
.
payable.transfer
EthPool
and EthVault
both use payable(address).transfer
to transfer ETH.
It's considered a best practice to avoid this pattern for ETH transfers, since it forwards a fixed amount of gas and may revert if future gas costs change. (See the Consensys Diligence article here).
function _doTransferOut(address payable to, uint256 amount) internal override { to.transfer(amount); }
function _transfer(address to, uint256 amount) internal override { payable(to).transfer(amount); }
function _depositToTreasury(uint256 amount) internal override { payable(addressProvider.getTreasury()).transfer(amount); }
Consider using OpenZeppelin Address.sendValue
, but take care to avoid reentrancy. Callers of these internal functions should be protected with a reentrancy guard.
Depositors to the GasBank
can accidentally make a deposit on behalf of address(0)
:
/** * @notice Deposit `msg.value` on behalf of `account` */ function depositFor(address account) external payable override { _balances[account] += msg.value; emit Deposit(account, msg.value); }
Since withdrawals check that msg.sender
matches the given withdrawal account, deposits credited to address(0)
cannot be recovered. Consider checking that the deposit account is not address(0)
.
StakerVault#transferFrom
The error message on line 152 of StakerVault.sol
checks the user's allowance, but returns an "insufficient balance" error message:
/* Get the allowance, infinite for the account owner */ uint256 startingAllowance = 0; if (spender == src) { startingAllowance = type(uint256).max; } else { startingAllowance = _allowances[src][spender]; } require(startingAllowance >= amount, Error.INSUFFICIENT_BALANCE); // Should this be 'insufficient allowance?'
Although this contract is explicitly not an ERC20 token, consider an "insufficient allowance" error, which is more consistent with user expectations.
The roleManager
local variable shadows AuthorizationBase.roleManager()
in initialize
.
function initialize(address roleManager) external initializer { AddressProviderMeta.Meta memory meta = AddressProviderMeta.Meta(true, true); _addressKeyMetas.set(AddressProviderKeys._ROLE_MANAGER_KEY, meta.toUInt()); _setConfig(AddressProviderKeys._ROLE_MANAGER_KEY, roleManager); }
The roleManager
local variable shadows AuthorizationBase.roleManager()
in Authorization#constructor
.
constructor(IRoleManager roleManager) { __roleManager = roleManager; // shadowed variable in constructor }
SwapperRegistry#swapperExists
uses an unnecessary ternary operator:
/** * @notice Check if a swapper implementation exists for a given token pair. * @param fromToken Address of token to swap. * @param toToken Address of token to receive. * @return True if a swapper exists for the token pair. */ function swapperExists(address fromToken, address toToken) external view override returns (bool) { return _swapperImplementations[fromToken][toToken] != address(0) ? true : false; }
BkdLocker.govToken
is instantiated as an IBkdToken
but stored as an IERC20
.
IERC20 public immutable govToken; constructor( address _rewardToken, address _govToken, IRoleManager roleManager ) Authorization(roleManager) { rewardToken = _rewardToken; govToken = IBkdToken(_govToken); }
Functions missing natspec documentation:
Consider adding events for:
Errors.sol
and IController.sol
imports are duplicated in StakerVault.sol
.
#0 - chase-manning
2022-04-28T10:05:43Z
I consider this report to be of particularly high quality
🌟 Selected for report: joestakey
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0xDjango, 0xNazgul, 0xkatana, 0xmint, Dravee, Funen, IllIllI, MaratCerby, NoamYakov, Tadashi, TerrierLover, Tomio, WatchPug, catchup, defsec, fatherOfBlocks, hake, horsefacts, kenta, oyc_109, pauliax, rayn, rfa, robee, saian, securerodd, simon135, slywaters, sorrynotsorry, tin537, z3s
84.957 USDC - $84.96
The while
loop inside BkdLocker#executeUnlocks
can safely use an unchecked decrement.
Impact: 63 gas per loop iteration.
require(length > 0, "No entries"); uint256 i = length; while (i > 0) { i = i - 1; if (stashedWithdraws[i].releaseTime <= block.timestamp) { totalAvailableToWithdraw += stashedWithdraws[i].amount; stashedWithdraws[i] = stashedWithdraws[stashedWithdraws.length - 1]; stashedWithdraws.pop(); } }
Using unchecked decrement:
require(length > 0, "No entries"); uint256 i = length; while (i > 0) { unchecked { i = i - 1; } if (stashedWithdraws[i].releaseTime <= block.timestamp) { totalAvailableToWithdraw += stashedWithdraws[i].amount; stashedWithdraws[i] = stashedWithdraws[stashedWithdraws.length - 1]; stashedWithdraws.pop(); } }
StakerVault#transferFrom
Since earlier require
statements in StakerVault#transferFrom
ensure that startingAllowance
and srcTokens
are >=
the specified transfer amount
, subtraction to calculate new allowance and balance can be performed safely using unchecked math.
Impact: 190 gas.
require(startingAllowance >= amount, Error.INSUFFICIENT_BALANCE); uint256 srcTokens = balances[src]; require(srcTokens >= amount, Error.INSUFFICIENT_BALANCE); address lpGauge = currentAddresses[_LP_GAUGE]; if (lpGauge != address(0)) { ILpGauge(lpGauge).userCheckpoint(src); ILpGauge(lpGauge).userCheckpoint(dst); } ILiquidityPool pool = controller.addressProvider().getPoolForToken(token); pool.handleLpTokenTransfer(src, dst, amount); uint256 allowanceNew = startingAllowance - amount; // Cannot underflow due to first require on L#152 uint256 srcTokensNew = srcTokens - amount; // Cannot underflow due to second require on L#155 uint256 dstTokensNew = balances[dst] + amount;
Using unchecked math:
require(startingAllowance >= amount, Error.INSUFFICIENT_BALANCE); uint256 srcTokens = balances[src]; require(srcTokens >= amount, Error.INSUFFICIENT_BALANCE); address lpGauge = currentAddresses[_LP_GAUGE]; if (lpGauge != address(0)) { ILpGauge(lpGauge).userCheckpoint(src); ILpGauge(lpGauge).userCheckpoint(dst); } ILiquidityPool pool = controller.addressProvider().getPoolForToken(token); pool.handleLpTokenTransfer(src, dst, amount); unchecked { uint256 allowanceNew = startingAllowance - amount; uint256 srcTokensNew = srcTokens - amount; } uint256 dstTokensNew = balances[dst] + amount;