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: 17/60
Findings: 5
Award: $666.28
🌟 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
https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/pool/EthPool.sol#L30 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/vault/EthVault.sol#L29 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/vault/VaultReserve.sol#L81
Transfer ETH by using transfer()
may cause this transaction to fail.
In EIP-1884:
In many cases, a recipient of ether
from a CALL
will want to issue a LOG
. The LOG
operation costs 375
plus 375
per topic. If the LOG
also wants to do an SLOAD
, this change may make some such transfers fail.
contract Vulnerable { function withdraw(uint256 amount) external { // This forwards 2300 gas, which may not be enough if the recipient // is a contract and gas costs change. msg.sender.transfer(amount); } }
ref: https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
vim, ganache-cli
Use call{value: amount}()
instead of transfer
:
(bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Address: unable to send value, recipient may have reverted");
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L60
#0 - chase-manning
2022-04-28T11:41:16Z
Duplicate of #52
293.0606 USDC - $293.06
https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/access/RoleManager.sol#L155 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/access/RoleManager.sol#L44 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/access/RoleManager.sol#L127
In RoleManager.sol
, _revokeRole()
doesn’t remove account
from _roleMembers[role]
. getRoleMember()
and getRoleMemberCount()
could then return wrong information. It could result in many problems, e.g., misinformation.
And the most critical problem lies in renounceGovernance()
. Since account
is not removed from _roleMembers[role]
after calling _revokeRole()
. getRoleMemberCount()
will return the same result after calling renounceGovernance()
. Then In renounceGovernance()
, getRoleMemberCount(Roles.GOVERNANCE) > 1
is still true even if there is only one governor. In consequence, governors could accidentally remove all the governors of the contract.
_roleMembers[role]
https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/access/RoleManager.sol#L44
function renounceGovernance() external onlyGovernance { require(getRoleMemberCount(Roles.GOVERNANCE) > 1, Error.CANNOT_REVOKE_ROLE); _revokeRole(Roles.GOVERNANCE, msg.sender); }
https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/access/RoleManager.sol#L155
function _revokeRole(bytes32 role, address account) internal { _roles[role].members[account] = false; emit RoleRevoked(role, account, msg.sender); }
getRoleMemberCount(Roles.GOVERNANCE) > 1
is still passed, since _roleMembers[role].length()
doesn’t change after bob calling _revokeRole()
.https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/access/RoleManager.sol#L127
function getRoleMemberCount(bytes32 role) public view virtual override returns (uint256) { if ( role == Roles.ADDRESS_PROVIDER || role == Roles.POOL_FACTORY || role == Roles.CONTROLLER ) { return 1; } if (role == Roles.POOL) { return addressProvider.poolsCount(); } if (role == Roles.VAULT) { return addressProvider.vaultsCount(); } return _roleMembers[role].length(); }
vim
account
should be removed from _roleMembers[role]
#0 - chase-manning
2022-04-28T11:48:07Z
Duplicate of #164
🌟 Selected for report: cccz
Also found by: 0x1f8b, 0xDjango, 0xkatana, Dravee, IllIllI, WatchPug, berndartmueller, defsec, horsefacts, hyh, kenta, rayn, reassor, sorrynotsorry
58.8714 USDC - $58.87
Though it checks updatedAt
with stalePriceDelay
, it still gets an unintended stale price without checking roundId
.
In getPriceUSD
function of ChainlinkOracleProvider.sol:
(, int256 answer, , uint256 updatedAt, ) = AggregatorV2V3Interface(feed).latestRoundData(); require(block.timestamp <= updatedAt + stalePriceDelay, Error.STALE_PRICE); require(answer >= 0, Error.NEGATIVE_PRICE);
latestRoundData()
also returns roundId
and answeredInRound
, it's not safe to only check updateAt
.
vim, ethers.js
Also check roundId
when getting price:
+ (uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = AggregatorV2V3Interface(feed).latestRoundData(); require(block.timestamp <= updatedAt + stalePriceDelay, Error.STALE_PRICE); require(answer >= 0, Error.NEGATIVE_PRICE); + require(answeredInRound >= roundID, Error.STALE_PRICE);
#0 - chase-manning
2022-04-28T11:27:42Z
Duplicate of #17
🌟 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
159.3125 USDC - $159.31
It’s better to use the trusted 3rd party library SafeERC20 to check the return value of transfer
.
vim, ethers.js
Use SafeERC20 rather than checking manually.
🌟 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 for loop has no overflow risk of i
. Use an unchecked block to save gas.
access/RoleManager.sol 80: for (uint256 i = 0; i < roles.length; i++) { strategies/ConvexStrategyBase.sol 313: for (uint256 i = 0; i < _rewardTokens.length(); i++) { 380: for (uint256 i = 0; i < _rewardTokens.length(); i++) { BkdLocker.sol 310: for (uint256 i = 0; i < length; i++) { StakerVault.sol 260: for (uint256 i = 0; i < actions.length; i++) { testing/MockStableSwap.sol 30: for (uint256 i = 0; i < 3; i++) { 42: for (uint256 i = 0; i < 2; i++) { 70: for (uint256 i = 0; i < 3; i++) { Controller.sol 117: for (uint256 i = 0; i < numActions; i++) { actions/topup/TopUpAction.sol 188: for (uint256 i = 0; i < protocols.length; i++) { 456: for (uint256 i = 0; i < length; i++) { 479: for (uint256 i = 0; i < length; i++) { 506: for (uint256 i = 0; i < howMany; i++) { 891: for (uint256 i = 0; i < length; i++) { actions/topup/handlers/CTokenRegistry.sol 61: for (uint256 i = 0; i < ctokens.length; i++) { actions/topup/handlers/CompoundHandler.sol 135: for (uint256 i = 0; i < assets.length; i++) { actions/topup/TopUpKeeperHelper.sol 43: for (uint256 i = 0; i < users.length; i++) { 46: for (uint256 j = 0; j < positions.length; j++) { 72: for (uint256 i = 0; i < keys.length; i++) { 93: for (uint256 i = 0; i < length; i++) { 165: for (uint256 i = 0; i < length; i++) { tokenomics/VestedEscrow.sol 93: for (uint256 i = 0; i < amounts.length; i++) { tokenomics/KeeperGauge.sol 155: for (uint256 i = startEpoch; i < endEpoch; i++) { tokenomics/InflationManager.sol 91: for (uint256 i = 0; i < length; i++) { 105: for (uint256 i = 0; i < length; i++) { 109: for (uint256 i = 0; i < stakerVaults.length; i++) { 114: for (uint256 i = 0; i < length; i++) { 166: for (uint256 i = 0; i < length; i++) { 191: for (uint256 i = 0; i < length; i++) { 259: for (uint256 i = 0; i < length; i++) { 283: for (uint256 i = 0; i < length; i++) { 357: for (uint256 i = 0; i < length; i++) { 381: for (uint256 i = 0; i < length; i++) { 404: for (uint256 i = 0; i < length; i++) { 445: for (uint256 i = 0; i < length; i++) {
Use unchecked
blocks to avoid overflow checks, or use ++i
rather than i++
if you don't use unchecked blocks.
for (uint256 i = 0; i < length; ) { ... unchecked { ++i; } }