Platform: Code4rena
Start Date: 12/08/2022
Pot Size: $50,000 USDC
Total HM: 15
Participants: 120
Period: 5 days
Judge: Justin Goro
Total Solo HM: 6
Id: 153
League: ETH
Rank: 3/120
Findings: 4
Award: $4,184.70
π Selected for report: 2
π Solo Findings: 1
π Selected for report: berndartmueller
2535.6587 USDC - $2,535.66
The ownership of a deployed Fraxlend pair is transferred to COMPTROLLER_ADDRESS
on deployment via FraxlendPairDeployer_deploySecond
. This very owner is able to change the currently used time lock contract address with the FraxlendPair.setTimeLock
function. A time lock is enforced on the FraxlendPair.changeFee
function whenever the protocol fee is adjusted.
However, as the Fraxlend pair owner is able to change the time lock contract address to any other arbitrary (contract) address, it is possible to circumvent this timelock without users knowing. By using a custom smart contract without an enforced time lock, the protocol fee can be changed at any time without a proper time lock.
/// @notice The ```setTimeLock``` function sets the TIME_LOCK address /// @param _newAddress the new time lock address function setTimeLock(address _newAddress) external onlyOwner { emit SetTimeLock(TIME_LOCK_ADDRESS, _newAddress); TIME_LOCK_ADDRESS = _newAddress; }
Manual review
Currently, the owner COMPTROLLER_ADDRESS
address is trustworthy, however, nothing prevents the above-described scenario. To protect users from sudden protocol fee changes, consider using a minimal time lock implementation directly implemented in the contract without trusting any external contract.
#0 - DrakeEvans
2022-09-06T11:13:36Z
Disagree with severity the only thing the timelock does is change the fee amount. The owner of the pair is the fraxlend comptroller and is a trusted address.
#1 - gititGoro
2022-10-02T03:42:33Z
Although the address is trusted, a changing of fees quicker than users anticipate is potentially a leakage of value beyond protocol expectations. So the severity will be maintained.
π Selected for report: 0x52
Also found by: Lambda, berndartmueller, cryptphi
https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L137 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L141 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L145 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L149
The FraxlendPair
contract implements the EIP4626
standard (EIP-4626: Tokenized Vault Standard).
However, according to EIP4626
, the below-mentioned functions do not fully adhere to the specs.
function maxDeposit(address) external pure returns (uint256) { return type(uint128).max; }
2 ** 256 - 1
if there is no limit on the maximum amount of assets that may be depositedfunction maxMint(address) external pure returns (uint256) { return type(uint128).max; }
2 ** 256 - 1
if there is no limit on the maximum amount of shares that may be mintedfunction maxWithdraw(address owner) external view returns (uint256) { return totalAsset.toAmount(balanceOf(owner), false); }
function maxRedeem(address owner) external view returns (uint256) { return balanceOf(owner); }
Manual review
Consider implementing the EIP4626
standard as close to the specs as possible.
#0 - amirnader-ghazvini
2022-08-29T19:17:25Z
Duplicate of #79
π Selected for report: berndartmueller
Also found by: IllIllI
1141.0464 USDC - $1,141.05
https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairDeployer.sol#L205 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairDeployer.sol#L329-L330 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairDeployer.sol#L253
The FraxlendPairDeployer
contract used to deploy Fraxlend pairs prevents deploying pairs with the same salt
and _configData
. This makes it vulnerable to front-running pair deployments and prevents deploying honest Fraxlend pairs.
FraxlendPairDeployer._deployFirst
function _deployFirst( bytes32 _saltSeed, bytes memory _configData, bytes memory _immutables, uint256 _maxLTV, uint256 _liquidationFee, uint256 _maturityDate, uint256 _penaltyRate, bool _isBorrowerWhitelistActive, bool _isLenderWhitelistActive ) private returns (address _pairAddress) { { // _saltSeed is the same for all public pairs so duplicates cannot be created bytes32 salt = keccak256(abi.encodePacked(_saltSeed, _configData)); require(deployedPairsBySalt[salt] == address(0), "FraxlendPairDeployer: Pair already deployed"); // @audit-info Reverts if a pair with the same salt is already deployed bytes memory _creationCode = BytesLib.concat( SSTORE2.read(contractAddress1), SSTORE2.read(contractAddress2) ); bytes memory bytecode = abi.encodePacked( _creationCode, abi.encode( _configData, _immutables, _maxLTV, _liquidationFee, _maturityDate, _penaltyRate, _isBorrowerWhitelistActive, _isLenderWhitelistActive ) ); assembly { _pairAddress := create2(0, add(bytecode, 32), mload(bytecode), salt) } require(_pairAddress != address(0), "FraxlendPairDeployer: create2 failed"); deployedPairsBySalt[salt] = _pairAddress; } return _pairAddress; }
A deployed Fraxlend pair address is stored in the deployedPairsBySalt
mapping using a salt
as the key. This salt is generated based on a seed _saltSeed
and _configData
. Deploying a standard Fraxlend pair with FraxlendPairDeployer.deploy
uses _saltSeed = keccak256(abi.encodePacked("public"))
and user-defined _configData
. The salt is the same for all standard deployments.
function deploy(bytes memory _configData) external returns (address _pairAddress) { (address _asset, address _collateral, , , , address _rateContract, ) = abi.decode( _configData, (address, address, address, address, uint256, address, bytes) ); string memory _name = string( abi.encodePacked( "FraxlendV1 - ", IERC20(_collateral).safeName(), "/", IERC20(_asset).safeName(), " - ", IRateCalculator(_rateContract).name(), " - ", (deployedPairsArray.length + 1).toString() ) ); _pairAddress = _deployFirst( keccak256(abi.encodePacked("public")), _configData, abi.encode(CIRCUIT_BREAKER_ADDRESS, COMPTROLLER_ADDRESS, TIME_LOCK_ADDRESS, FRAXLEND_WHITELIST_ADDRESS), DEFAULT_MAX_LTV, DEFAULT_LIQ_FEE, 0, 0, false, false ); _deploySecond(_name, _pairAddress, _configData, new address[](0), new address[](0)); _logDeploy(_name, _pairAddress, _configData, DEFAULT_MAX_LTV, DEFAULT_LIQ_FEE, 0); }
A whitelisted (and potentially dishonest) user could watch the mempool for new Fraxlend pair deployments and front-run deployments with a custom pair deployment by calling FraxlendPairDeployer.deployCustom
.
FraxlendPairDeployer.deployCustom
function deployCustom( string memory _name, bytes memory _configData, uint256 _maxLTV, uint256 _liquidationFee, uint256 _maturityDate, uint256 _penaltyRate, address[] memory _approvedBorrowers, address[] memory _approvedLenders ) external returns (address _pairAddress) { require(_maxLTV <= GLOBAL_MAX_LTV, "FraxlendPairDeployer: _maxLTV is too large"); require( IFraxlendWhitelist(FRAXLEND_WHITELIST_ADDRESS).fraxlendDeployerWhitelist(msg.sender), "FraxlendPairDeployer: Only whitelisted addresses" ); _pairAddress = _deployFirst( keccak256(abi.encodePacked(_name)), _configData, abi.encode(CIRCUIT_BREAKER_ADDRESS, COMPTROLLER_ADDRESS, TIME_LOCK_ADDRESS, FRAXLEND_WHITELIST_ADDRESS), _maxLTV, _liquidationFee, _maturityDate, _penaltyRate, _approvedBorrowers.length > 0, _approvedLenders.length > 0 ); _deploySecond(_name, _pairAddress, _configData, _approvedBorrowers, _approvedLenders); deployedPairCustomStatusByAddress[_pairAddress] = true; _logDeploy(_name, _pairAddress, _configData, _maxLTV, _liquidationFee, _maturityDate); }
Internally the FraxlendPairDeployer.deployCustom
uses the same _deployFirst
function as a standard pair deployment uses. However, the salt is user-definable by using any _name
, for instance public
. Then the salt is the same as _saltSeed = keccak256(abi.encodePacked("public"))
. The user then uses the same _configData
as the to be deployed pair. But, contrary to a standard pair deployment, a whitelisted user is also able to provide additional configuration, _maxLTV
, _liquidationFee
and so on.
The whitelisted user is able to deploy a custom Fraxlend pair with the same salt and the same _configData
but by using for instance a _maturityDate < block.timestamp
, this deployed custom pool is unusable (due to the modifier isNotPastMaturity
reverting. This modifier is used by FraxlendPairCore.deposit
, FraxlendPairCore.mint
, FraxlendPairCore.borrowAsset
, FraxlendPairCore.addCollateral
and FraxlendPairCore.leveragedPosition
).
As FraxlendPairDeployer._deployFirst
checks if a pair is already deployed with a given salt, the honest pair deployment that got front-run reverts. It is now not possible to deploy a pair with the same configuration.
NOTE: The same can be achieved by front-running a pair deployment transaction with a custom pair deployment and with the same name (FraxlendPairDeployer.deployCustom
allows providing an arbitrary name . FraxlendPairDeployer._deploySecond
checks the name of the pair and reverts if a pair with that name already exists:
FraxlendPairDeployer._deploySecond
function _deploySecond( string memory _name, address _pairAddress, bytes memory _configData, address[] memory _approvedBorrowers, address[] memory _approvedLenders ) private { (, , , , , , bytes memory _rateInitData) = abi.decode( _configData, (address, address, address, address, uint256, address, bytes) ); require(deployedPairsByName[_name] == address(0), "FraxlendPairDeployer: Pair name must be unique"); // @audit-info reverts if a pair with the same name exists already deployedPairsByName[_name] = _pairAddress; deployedPairsArray.push(_name); // Set additional values for FraxlendPair IFraxlendPair _fraxlendPair = IFraxlendPair(_pairAddress); _fraxlendPair.initialize(_name, _approvedBorrowers, _approvedLenders, _rateInitData); // Transfer Ownership of FraxlendPair _fraxlendPair.transferOwnership(COMPTROLLER_ADDRESS); }
Manual review
Consider validating the _name
parameter in the FraxlendPairDeployer.deployCustom
function to not equal the string public
, thus preventing the usage of the same salt as a standard pair deployment.
Additionally, consider prefixing the name of a custom pair deployment to also prevent front-running this way.
#0 - DrakeEvans
2022-09-06T11:18:20Z
This would only prevent the deployment of 1 pair, disagree with severity. The same pair could be deployed using custom deployment by team if necessary.
#1 - gititGoro
2022-10-02T04:29:48Z
The availability of the protocol is still affected and a disruption of service can occur. In sympathy to the warden not knowing the nature of the whitelist upfront, severity will be maintained.
π Selected for report: 0x1f8b
Also found by: 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xmatt, 0xsolstars, Aymen0909, Bnke0x0, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, EthLedger, Funen, IllIllI, JC, Junnon, Lambda, LeoS, MiloTruck, Noah3o6, PaludoX0, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, SaharAP, Sm4rty, SooYa, The_GUILD, TomJ, Waze, Yiko, _Adam, __141345__, a12jmx, ak1, asutorufos, auditor0517, ayeslick, ballx, beelzebufo, berndartmueller, bin2chen, brgltd, c3phas, cRat1st0s, cccz, cryptonue, cryptphi, d3e4, delfin454000, dipp, djxploit, durianSausage, dy, erictee, fatherOfBlocks, gogo, gzeon, hyh, ignacio, kyteg, ladboy233, medikko, mics, minhquanym, oyc_109, pfapostol, rbserver, reassor, ret2basic, robee, sach1r0, simon135, sryysryy, tabish, yac, yash90, zzzitron
45.8684 USDC - $45.87
https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L288-L296 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L307-L315
Whitelisted addresses are able to deploy customized pairs. One of the customizations is the enforcement of a lender and borrower allowlist and blocklist by providing an array of approved borrowers _approvedBorrowers
and/or an array of the approved lenders _approvedLenders
.
Lender and borrowers can be approved/blocked by invoking the functions FraxlendPair.setApprovedLenders
or FraxlendPair.setApprovedBorrowers
. Only approved lenders, as well as approved borrowers, are able to call those functions.
However, an approved lender is able to approve and block any other arbitrary address. Same for borrowers. If any of the approved lenders or borrowers turn dishonest and malicious, this single actor is able to front-run and block all other addresses and prevent them from using the deployed pair.
Blocked lenders are not able to use the following functions:
Blocked borrowers are not able to use the following functions:
function setApprovedLenders(address[] calldata _lenders, bool _approval) external approvedLender(msg.sender) { for (uint256 i = 0; i < _lenders.length; i++) { // Do not set when _approval == false and _lender == msg.sender if (_approval || _lenders[i] != msg.sender) { approvedLenders[_lenders[i]] = _approval; emit SetApprovedLender(_lenders[i], _approval); } } }
function setApprovedBorrowers(address[] calldata _borrowers, bool _approval) external approvedBorrower { for (uint256 i = 0; i < _borrowers.length; i++) { // Do not set when _approval == false and _borrower == msg.sender if (_approval || _borrowers[i] != msg.sender) { approvedBorrowers[_borrowers[i]] = _approval; emit SetApprovedBorrower(_borrowers[i], _approval); } } }
Manual review
Consider allowing only the owner of the deployed pair to allow/block addresses. Otherwise, the list of approved lenders/borrowers can grow very fast and the chance of having a bad actor as one of the approved addresses, is very high. It only takes a single bad actor to denial-of-service the deployed pair.
#0 - 0xA5DF
2022-08-17T20:58:01Z
Duplicate of #135
#1 - DrakeEvans
2022-09-06T12:31:50Z
This is the intended design, approvedBorrower and approvedLenders are meant to represent a single entity. Like a single DAO, the ability to have multiple addresses is a convenience but ultimately the responsibility of each side of the contract lies with the borrower or lender.
#2 - gititGoro
2022-09-27T13:43:59Z
Both the sponsor and the wardens have solid arguments to make in support of this issue being either valid or invalid. On the one hand, this feature is by design as the sponsor has pointed out and is not in practice open to abuse. For this reason, it could be considered invalid. However, the wardens are not wrong to point out that giving peer contracts co-admin rights presents a horizontal risk.
Ultimately the risk is largely mitigated by the fact that the entrance into becoming an approved entity is via the initial whitelist. As long as the deployer has tight control over the initial whitelist, this should be the case.
Nonetheless, the horizontal risk is amplified with every newly approved lender/borrower. It only takes one mistake to destroy the whole system. So it would be best practice to employ a hierarchy of admin granting such as owner->approved whitelisting.
This issue and all the duplicates will be converted to QA and linked to existing QA issues submitted by these wardens.
TL;DR It is not good practice to allow for horizontal admin granting but it suits the need of the Frax DAO and the sponsor has intentionally chosen this design. The feature will be treated as a security smell