Platform: Code4rena
Start Date: 22/09/2022
Pot Size: $30,000 USDC
Total HM: 12
Participants: 133
Period: 3 days
Judge: 0xean
Total Solo HM: 2
Id: 165
League: ETH
Rank: 35/133
Findings: 3
Award: $72.55
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: ladboy233
Also found by: 0xSmartContract, 8olidity, Aymen0909, Chom, IllIllI, OptimismSec, PaludoX0, TomJ, ayeslick, cccz, csanuragjain, joestakey, neko_nyaa, pashov, peritoflores, rbserver, rvierdiiev
19.982 USDC - $19.98
https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L191-L196
The function recoverEther()
does not have any checks to ensure the total supply of frxETH matches the held ETH after recovering. Therefore an admin (or the timelock) can (intentionally or unintentionally) withdraw ETH without burning frxETH.
The following test successfully runs when added to frxETHMinterTest
. FRAX_COMPTROLLER
successfully withdraws 5 Ethers deposited, while the user still has their original 5 frxETH minted.
function testBadRecover() public { address user = 0x1234567890123456789012345678901234567890; vm.startPrank(user); vm.deal(user, 5 ether); minter.submit{value: 5 ether}(); vm.stopPrank(); vm.startPrank(FRAX_COMPTROLLER); // Note the starting ETH balance of the comptroller uint256 starting_eth = FRAX_COMPTROLLER.balance; // Recover 5 ETH vm.expectEmit(false, false, false, true); emit EmergencyEtherRecovered(5 ether); minter.recoverEther(5 ether); // FRAX_COMPTROLLER gets 5 ether, but the user's frxETH balance is the same assertEq(FRAX_COMPTROLLER.balance, starting_eth + (5 ether)); assertEq(frxETHToken.balanceOf(user), 5 ether); vm.stopPrank(); }
A malicious or compromised admin can withdraw ETH directly without burning frxETH, inflating frxETH and de-pegging the token by its definition.
VSCode
If there were only one minter, this check would have been enough:
require(amount <= address(this).balance - frxETHToken.totalSupply() - currentWithheldETH, "Not enough for recover");
Basically we want to account for frxETH's total supply, as well as the current ETH withheld by the contract. Having multiple minters makes checking substantially more difficult. A few possible ideas are:
The third idea would be the simplest in my opinion, then such a check as shown above would be enough.
#0 - FortisFortuna
2022-09-25T21:23:24Z
We are well aware of the permission structure. The owner will most likely be a large multisig. We mentioned the Frax Multisig in the scope too. If moving funds, it is assumed someone in the multisig would catch an invalid or malicious address.
#1 - 0xean
2022-10-12T14:58:13Z
marking as dupe of #107
🌟 Selected for report: rotcivegaf
Also found by: 0x040, 0x1f8b, 0x4non, 0xNazgul, 0xSmartContract, 0xf15ers, 8olidity, Aymen0909, B2, Bahurum, Bnke0x0, Ch_301, CodingNameKiki, Deivitto, Diana, Funen, IllIllI, JC, JLevick, KIntern_NA, Lambda, OptimismSec, PaludoX0, RockingMiles, Rolezn, Sm4rty, Soosh, Tagir2003, Tointer, TomJ, Triangle, Trust, V_B, Waze, Yiko, __141345__, a12jmx, ajtra, asutorufos, ayeslick, aysha, bbuddha, bharg4v, bobirichman, brgltd, bytera, c3phas, cryptostellar5, cryptphi, csanuragjain, datapunk, delfin454000, durianSausage, exd0tpy, gogo, got_targ, jag, joestakey, karanctf, ladboy233, leosathya, lukris02, mics, millersplanet, natzuu, neko_nyaa, obront, oyc_109, parashar, peritoflores, rbserver, ret2basic, rokinot, ronnyx2017, rvierdiiev, sach1r0, seyni, sikorico, slowmoses, tnevler, yasir, yongskiws
39.7592 USDC - $39.76
https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L114-L116 https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L104-L106
frxETHMinter.receive()
is equivalent to submit()
, thereby there is no way for (e.g.) governance to submit ETH for withholding, except for forcefully sending ETH by self-destructing a contract.
The Owned
contract is designed such that any new owner must accept ownership, most likely to prevent mistaken transfers. However, this is not the case for the constructor, as owner
is set to a parameter address, as opposed to the deployer (msg.sender
).
https://github.com/code-423n4/2022-09-frax/blob/main/src/Utils/Owned.sol#L10-L14
The Owned.sol
file is out-of-scope, however both OperatorRegistry
and frxETHMinter
contracts uses the same method for setting the initial owner. Thus the deployed contract can be lost upon construction if the initial address is not appropriately set.
https://github.com/code-423n4/2022-09-frax/blob/main/src/OperatorRegistry.sol#L40-L43 https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L52-L65
For this reason I consider this a low-severity finding, and am opting to bring Owned.sol
into the scope for this specific case.
Recommended mitigation steps: Use msg.sender
as the initial owner, as opposed to a parameter.
Also consider using boringcrypto's BoringOwnable.sol, which enforces the initial owner to be the deployer. The contract has all functionalities needed, and it's also good practice to use prewritten tools that are known to be secure.
ERC20PermitPermissionedMint.sol
: It is possible to implement removeMinter()
without having to loop through the entire arrayLoop line: https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L84
In general, it is not good practice to loop through an entire storage array, as it would cause the tx to be gas-costly, or even reverting, although such a case would be quite extreme and not likely to happen.
This is considered a low finding because it has a gas optimization, a QA element, as well as a possible (but unlikely) bug.
Recommended mitigation steps: We can use the minters
mapping to store the ($1$-based) index of the minter, instead of simply a boolean flag. This will immediately tell us which position to set minters_array
to $0$, and also has the virtue of saving gas, for not having to read the entire storage array, as well as using uint256
is actually more gas-efficient than bool
.
Such an implementation can be as follow:
address[] public minters_array; mapping(address => uint256) public minters; // not bool function addMinter(address minter_address) public onlyByOwnGov { minters[minter_address] = minters_array.length + 1; minters_array.push(minter_address); } function removeMinter(address minter_address) public onlyByOwnGov { require(minters[minter_address] != 0, "Address nonexistant"); minters_array[minters[minter_address]] = address(0); delete minters[minter_address]; }
The onlyMinters
modifier would then also need to be changed, albeit that too is quite simple
modifier onlyMinters() { require(minters[msg.sender] != 0, "Only minters"); _; }
Alternatively, one can also take an approach to remove minter by index, instead of by address (similar to how OperatorRegistry.removeValidator()
works), so that the minters
mapping can retain a boolean-like value structure (however that would require finding the minter's index off-chain first).
safeTransfer
instead of transfer
for ERC20https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L200
OperatorRegistry.getNextValidator()
can be renamed to getLastValidatorAndPop()
https://github.com/code-423n4/2022-09-frax/blob/main/src/OperatorRegistry.sol#L126
frxETHMinter.depositEther()
can be renamed to deliverEtherToValidators()
, or depositWithheldETHToValidators()
:
https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L120
public
functions that are not called by the contract itself can be made external
Applies to all public
functions within ERC20PermitPermissionedMint.sol
https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L53 https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L59 https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L65 https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L76 https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L94
🌟 Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x4non, 0x5rings, 0xA5DF, 0xNazgul, 0xSmartContract, 0xmatt, 0xsam, Amithuddar, Aymen0909, B2, Ben, Bnke0x0, Chom, CodingNameKiki, Deivitto, Diana, Fitraldys, Funen, IllIllI, JAGADESH, JC, Metatron, Ocean_Sky, PaludoX0, Pheonix, RaymondFam, ReyAdmirado, RockingMiles, Rohan16, Rolezn, Satyam_Sharma, Sm4rty, SnowMan, SooYa, Tagir2003, TomJ, Tomio, Triangle, V_B, Waze, __141345__, ajtra, albincsergo, asutorufos, aysha, beardofginger, bobirichman, brgltd, bulej93, bytera, c3phas, ch0bu, cryptostellar5, cryptphi, d3e4, delfin454000, dharma09, drdr, durianSausage, emrekocak, erictee, fatherOfBlocks, gogo, got_targ, imare, jag, karanctf, ladboy233, leosathya, lukris02, medikko, mics, millersplanet, natzuu, neko_nyaa, oyc_109, peanuts, prasantgupta52, rbserver, ret2basic, rokinot, ronnyx2017, rotcivegaf, sach1r0, samruna, seyni, slowmoses, tnevler, wagmi, zishansami
12.8108 USDC - $12.81
bool
instead of uint256
causes extra gas overheadhttps://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L20 https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L43
array.length
in every iteration of the for loopIt is more gas-efficient to cache the length first, then compare them. E.g.
uint len = minters_array.length; for (uint i = 0; i < len; i++) { if (minters_array[i] == minter_address) { minters_array[i] = address(0); break; } }
Two applicable instances:
https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L83-L89 https://github.com/code-423n4/2022-09-frax/blob/main/src/OperatorRegistry.sol#L114
true
, use the value itselfe.g. This line of code
require(minters[msg.sender] == true, "Only minters");
Can be changed to
require(minters[msg.sender], "Only minters");
Or, if [G-01] is implemented
require(minters[msg.sender] != 0, "Only minters");
Two applicable instances
https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L46 https://github.com/code-423n4/2022-09-frax/blob/main/src/ERC20/ERC20PermitPermissionedMint.sol#L78
!= 0
instead of > 0
for uint
type is more gas-efficientTwo applicable instances
https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L79 https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L126