Platform: Code4rena
Start Date: 01/07/2022
Pot Size: $75,000 USDC
Total HM: 17
Participants: 105
Period: 7 days
Judge: Jack the Pug
Total Solo HM: 5
Id: 143
League: ETH
Rank: 18/105
Findings: 3
Award: $799.78
π Selected for report: 0
π Solo Findings: 0
π Selected for report: 0xNineDec
Also found by: 0x1f8b, 0x29A, 0x52, 0xDjango, 0xdanial, 0xf15ers, Cheeezzyyyy, Chom, Franfran, GalloDaSballo, Green, IllIllI, Meera, Ruhum, bardamu, cccz, codexploder, defsec, hake, hansfriese, horsefacts, hubble, hyh, jonatascm, kebabsec, oyc_109, pashov, rbserver, simon135, tabish, tintin, zzzitron
14.8726 USDC - $14.87
updatedAt field of Chainlink's latestRoundData() isn't checked, so even substantially outdated price will be used by the system.
Price is the only field that is read from Chainlink:
function currentPrice(uint256 _decimals) external view override returns (uint256) { // Get the latest round information. Only need the price is needed. (, int256 _price, , , ) = feed.latestRoundData(); // Get a reference to the number of decimals the feed uses. uint256 _feedDecimals = feed.decimals(); // Return the price, adjusted to the target decimals. return uint256(_price).adjustDecimals(_feedDecimals, _decimals); }
Consider adding a logic to filter out outdated or wrong submissions, for example:
(, int256 priceInUsd, , int256 updatedAt, ) = baseAggregator.latestRoundData(); require(priceInUsd > 0 && updatedAt > 0, "Price invalid"); // some checks that updatedAt isn't too far in the past
https://etherscan.io/address/0x37bc7498f4ff12c19678ee8fe19d713b87f6a9e6#code#F2#L786
#0 - mejango
2022-07-12T18:46:11Z
dup #138
π Selected for report: horsefacts
Also found by: 0x1f8b, 0x29A, 0x52, 0xf15ers, AlleyCat, Ch_301, Chom, Franfran, IllIllI, Kaiziron, Limbooo, Meera, Ruhum, Sm4rty, apostle0x01, berndartmueller, cccz, cloudjunky, codexploder, cryptphi, delfin454000, durianSausage, fatherOfBlocks, hake, hansfriese, hyh, jonatascm, m_Rassska, oyc_109, peritoflores, rajatbeladiya, rbserver, svskaushik, zzzitron
3.4075 USDC - $3.41
A number of functions (full list below) will not revert on the transfer call failure and can be inaccessible for tokens not fully compliant with OZ's IERC20 (say, USDT). Unifying this in one issue for all the affected functions.
When a token do not revert, but instead return false this will go unnoticed in the current implementation with the corresponding downstream system's failures on programmatic usage.
ERC20 compliance is expected by using transfer, while it is not always the case and non-compliant token will not be retrievable by the functions.
(2) means that such tokens will be frozen on the contract balance, and, as the functions mentioned are parts of core system functionality this means system unavailability and fund freezing: say Bob the DAO manager moved funds to the system, but it cannot manage them, the functions are unavailable, the funds are frozen.
Setting impact to be high as that both can break up the functionality and produce yield losses as a part of token freezing.
The project use standard OZ implementation:
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
The transfer is done with unsafe function version despite the fact that arbitrary tokens are meant to be moved in the following functions:
pay:
// Transfer tokens to this contract from the msg sender. IERC20(_token).transferFrom(msg.sender, address(this), _amount);
IERC20(_token).transfer( // If there's a beneficiary, send the funds directly to the beneficiary. Otherwise send to the msg.sender. _beneficiary != address(0) ? _beneficiary : msg.sender, _leftoverAmount );
addToBalanceOf:
// Transfer tokens to this contract from the msg sender. IERC20(_token).transferFrom(msg.sender, address(this), _amount);
IERC20(_token).transfer( // If there's a default beneficiary, send the funds directly to the beneficiary. Otherwise send to the msg.sender. defaultBeneficiary != address(0) ? defaultBeneficiary : msg.sender, _leftoverAmount );
_payTo:
// Or, transfer the ERC20. else { IERC20(_token).transfer( // Get a reference to the address receiving the tokens. If there's a beneficiary, send the funds directly to the beneficiary. Otherwise send to _defaultBeneficiary. _split.beneficiary != address(0) ? _split.beneficiary : _defaultBeneficiary, _splitAmount ); }
pay:
// Transfer tokens to this contract from the msg sender. IERC20(_token).transferFrom(msg.sender, address(this), _amount);
addToBalanceOf:
// Transfer tokens to this contract from the msg sender. IERC20(_token).transferFrom(msg.sender, address(this), _amount);
_transferFrom:
_from == address(this) ? IERC20(token).transfer(_to, _amount) : IERC20(token).transferFrom(_from, _to, _amount);
To sum up:
The success of transfer operation isn't checked, so for tokens that do not revert on a failure, returning false instead, this failure will be ignored and downstream systems that rely on the functionality may malfunction (i.e. wrong internal accounting => insolvency kind of total impact).
Also, some tokens to not adhere to IERC20 fully, not returning the state at all (USDT as an example), for them the transfer will be failing as not complying to the interface:
https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code
Consider using safeTransfer for all ERC20 in/outbound transfers.
#0 - mejango
2022-07-12T18:33:13Z
#1 - jack-the-pug
2022-07-24T02:07:21Z
Duplicate of #242
π Selected for report: berndartmueller
781.4991 USDC - $781.50
Currently the fee is understated as the implementation calculates (1 / (1 + 1 / fee_share))
instead of fee_share
. It is not described in the documentation, common plain fee mechanics is declared, so it looks like it's unintentional.
Making it high as that's the base functionality that not working as expected with a cumulative negative impact on DAO fee earnings.
_feeAmount() calculates (1 / (1 + 1 / fee_share))
:
// The amount of tokens from the `_amount` to pay as a fee. return _amount - PRBMath.mulDiv(_amount, JBConstants.MAX_FEE, _discountedFee + JBConstants.MAX_FEE); }
It is _amount * (_discountedFee / (_discountedFee + JBConstants.MAX_FEE))
. If you denote fee_share = _discountedFee / JBConstants.MAX_FEE
, when _discountedFee > 0
the fraction can be divided by it and the returning value can be written as _amount * (1 / (1 + 1 / fee_share))
.
For example, when _discountedFee = JBConstants.MAX_FEE
it becomes _amount / 2
, when _discountedFee = JBConstants.MAX_FEE / 2
it becomes _amount / 3
and so on.
Plain fee percentage, _amount * fee_share
, is declared in the description:
@param _fee The percentage of the fee, out of MAX_FEE.
There looks to be nothing malicious in the formula, but plain fee mechanics will be expected by system users. I.e. when you set the fee to be 50%
of 5% = max_fee
, you expect 2.5%
, not 1.(6)%
.
Consider making the calculation fit the description and the common fee mechanics:
// The amount of tokens from the `_amount` to pay as a fee. return PRBMath.mulDiv(_amount, _discountedFee, JBConstants.MAX_FEE); }
#0 - mejango
2022-07-08T22:06:00Z
_amount
in this case is pre-fee. So an amount of 110 with a 10% fee should result in a fee of 10. 110 - (110 * 1/ 1.1) = 10.
#1 - dmitriia
2022-07-13T11:48:28Z
The _amount
is pre-fee by default. An amount of 110 with a 10% fee should result in a fee of 110 * 10%
= 11
. Generally fee isn't applied to an after-fee amount, it's applied directly to the incoming user funds, so a user can estimate that if x
fee is stated the x%
of supplied funds go to a fee, 1-x%
remains.
It is usual fee mechanics that vast majority of users will expect.
It is also declared in the function description:
/** @notice Returns the fee amount based on the provided amount for the specified project. @param *_amount The amount that the fee is based on*, as a fixed point number with the same amount of decimals as this terminal.