Platform: Code4rena
Start Date: 23/06/2023
Pot Size: $60,500 USDC
Total HM: 31
Participants: 132
Period: 10 days
Judge: 0xean
Total Solo HM: 10
Id: 254
League: ETH
Rank: 108/132
Findings: 1
Award: $18.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: alexweb3
Also found by: D_Auditor, DedOhWale, DelerRH, LuchoLeonel1, Musaka, Neon2835, Silvermist, Timenov, TorpedoPistolIXC41, adeolu, cartlex_, hals, josephdara, koo, lanrebayode77, mahyar, mladenov, mrudenko, pep7siup, zaevlad, zaggle
18.4208 USDC - $18.42
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L85 https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L90
OnlyRole and checkRole modifier want to guarantee only admin can call the function.
modifier onlyRole(bytes32 role) { GovernanceTimelock.checkOnlyRole(role, msg.sender); _; } modifier checkRole(bytes32 role) { GovernanceTimelock.checkRole(role, msg.sender); _; }
The implement of checkOnlyRole and checkRole are as follows:
function checkRole(bytes32 role, address _sender) public view returns(bool){ return hasRole(role, _sender) || hasRole(DAO, _sender); } function checkOnlyRole(bytes32 role, address _sender) public view returns(bool){ return hasRole(role, _sender); }
It return false when sender is not the admin. However, There's no require function in the previous modifier to check whether the return value is false, which turns out anyone can call the function protected by onlyRole and checkRole.
If anyone can call the function protected by onlyRole and checkRole. It will cause many security problems. I will show a critical attack chain in Proof of Concept.
One critical attack chain is using transferfrom to stole anyone's PeUSD. I'm sorry due to the lack of time and the lack of test in the contest. I haven't develop a test in there successfully. But I will try my best to explain the attack chain.
step 1: Attacker call setMintVault to set itself's active to true. Because the onlyRole have no check in fact. The setMintVault call will always success.
function setMintVault(address pool, bool isActive) external onlyRole(DAO) { mintVault[pool] = isActive; }
step 2: Attacker call PeUSD.transferFrom to stole anyone's PeUSD. Since the previous step set the attacker's mintVault to true. The if condition in [1] will be bypassed. Thus without any allowance check. Attack can transfer anyone's PeUSD.
function transferFrom(address from, address to, uint256 amount) public override returns (bool) { address spender = _msgSender(); if (!configurator.mintVault(spender)) { -----------------[1] _spendAllowance(from, spender, amount); } _transfer(from, to, amount); return true; }
vscode
Use require in both the modifier.
require( GovernanceTimelock.checkOnlyRole(role, msg.sender), "not authorized"); require( GovernanceTimelock.checkRole(role, msg.sender), "not authorized");
Access Control
#0 - c4-pre-sort
2023-07-04T12:51:14Z
JeffCX marked the issue as duplicate of #704
#1 - c4-judge
2023-07-28T15:24:04Z
0xean marked the issue as satisfactory