Platform: Code4rena
Start Date: 30/11/2021
Pot Size: $30,000 USDC
Total HM: 0
Participants: 21
Period: 3 days
Judge: pauliax
Id: 63
League: ETH
Rank: 6/21
Findings: 2
Award: $1,550.17
π Selected for report: 4
π Solo Findings: 0
1472.4455 USDC - $1,472.45
0x0x0x
When requery
is called, token1OutBase
can get updated. token1OutBase
is a crucial parameter, which directly tells how many tokens will the user get.
If a user creates a transaction and before getting included in a block, token1OutBase
gets reduced. Users will swap for an unexpected ratio and lose funds..
For swap operations in most contracts (like sushi), there is an input parameter for minimum outputted tokens. This ensures that the user can make swap requests with deterministic results. This protects users against such problems.
As seen in https://github.com/fei-protocol/fei-protocol-core/blob/cc14a601d60e6d550bc88ec84da0999fa8a3daf0/contracts/oracle/collateralization/CollateralizationOracleWrapper.sol#L128, minProtocolEquity
can be set by governor or admin arbitrarily (indirectly also token1OutBase
). On Oracle CachedValueUpdate
event is emitted, but there is also no guarantee, if someone calls requery
with new values or not. So users can easily get frontrunned or swap with an unexpected ratio.
Users don't have enough control when calling swap operation ngmi
. There is also no protection for users against unexpected behaviour. I consider this a high-risk issue.
I strongly recommend adding events, view functions and a minimum output parameter for ngmi
. Also, it would be nice to have a view function for token1OutBase
and token0InBase
. Minimum output parameter is utilized on almost all popular contracts with similar functionality, since it is crucial for user safety.
#0 - elee1766
2021-12-06T04:19:07Z
this is intended as far as i understand.
i see no reason to add a minimum output parameter as if minprotocolequity is set by admin/governor before the ragequit, the caller should lose their funds. that seems fine to me.
FEI may be burnt by the burner, so i see no real real protection added.
there is no need to add any events or view functions, the chain is available to be queried.
#1 - pauliax
2021-12-11T13:02:20Z
As per the sponsor's comment, this is the intended behavior. Because the price is always adjusted down, users are incentivized to ngmi as fast as possible, and having an up-to-date minProtocolEquity sounds fair. However, I agree that slippage protection is a common practice to give users more control but in this case, it is not that severe, so I am assigning this issue a severity of low.
7.2412 USDC - $7.24
0x0x0x
merkleRoot
is defined with 0 value and then assigned in constructor. The best approach for having a such parameter is using immutable
keyword. Also operations using merkleRoot
will consume less gas.
#0 - elee1766
2021-12-06T04:13:27Z
ack'd.
it should really be a constant and should be on final deploy
#1 - pauliax
2021-12-11T11:09:00Z
Similar to #147.
0x0x0x
Applying == true
doesn't make any difference and consume extra gas. Since this operation can be seen as an identity function.
./PegExchanger.sol:38: require(isEnabled() == true, "Proposals are not both passed"); ./PegExchanger.sol:111: isEnabled() == true, ./TRIBERagequit.sol:65: require(isEnabled() == true, "Proposals are not both passed"); ./TRIBERagequit.sol:69: verifyClaim(thisSender, key, merkleProof) == true,
#0 - elee1766
2021-12-06T04:10:36Z
#160
#1 - pauliax
2021-12-10T18:29:55Z
A duplicate of #160
0x0x0x
PegExchanger.sol#takeFrom
, PegExchanger.sol#giveTo
and TRIBERagequit.sol#takeFrom
are implemented in a similar manner. After transferFrom
call, the return value is cached to a parameter called check
, after that it is controlled whether check
is true
. But the used transferFrom
functions never return false
. They simply revert if there is a problem with the transaction. Therefore, it is possible to implement those functions more gas efficient.
Therefore following implementation:
function takeFrom(address target, uint256 amount) internal { bool check = token0.transferFrom(target, address(this), amount); require(check, "erc20 transfer failed"); }
Can be replaced with:
function takeFrom(address target, uint256 amount) internal { token0.transferFrom(target, address(this), amount); }
#0 - elee1766
2021-12-06T04:06:23Z
#120
#1 - pauliax
2021-12-11T09:17:29Z
Valid optimization. Making this a primary issue, as it covers both cases: inline transfer and removing the boolean check.
0x0x0x
When a variable is declared solidity assigns the default value. In case the contract assigns the value again, it costs extra gas.
Example:uint x = 0
costs more gas than uint x
without having any different functionality.
./PegExchanger.sol:22: uint256 public expirationBlock = 0; ./TRIBERagequit.sol:20: uint256 public expirationBlock = 0; ./TRIBERagequit.sol:26: uint256 public token1OutBase = 0; ./PegExchanger.sol:14: bool public party0Accepted = false; // rgt timelock accepted ./PegExchanger.sol:15: bool public party1Accepted = false; // tribe timelock accepted ./TRIBERagequit.sol:22: bool public party0Accepted = false; // rgt timelock accepted ./TRIBERagequit.sol:23: bool public party1Accepted = false; // tribe timelock accepted ./TRIBERagequit.sol:40: bool public init = false;
#0 - elee1766
2021-12-06T04:20:08Z
#157
#1 - pauliax
2021-12-11T09:56:38Z
A duplicate of #157
π Selected for report: 0x0x0x
Also found by: GeekyLumberjack, WatchPug
0x0x0x
The returned value is not used. Both functions update global variables and there is no need for a return.
#0 - elee1766
2021-12-06T04:13:02Z
#120
#1 - pauliax
2021-12-11T12:07:23Z
Valid suggestion. Making this a primary issue, because it mentions both functions, recalculate and requery.
0x0x0x
External
functions usually consume less gas compared to public
functions.
In PegExchange
:
exchange party0Accept party1Accept setExpirationBlock
In TRIBERagequit
:
ngmi requery party0Accept party1Accept
#0 - elee1766
2021-12-06T03:05:13Z
duplicate #27
#1 - pauliax
2021-12-11T10:25:16Z
A duplicate of #27
π Selected for report: 0x0x0x
Also found by: Meta0xNull, WatchPug
19.8662 USDC - $19.87
0x0x0x
Example:
for (uint i = 0; i < arr.length; i++) { //Operations not effecting the length of the array. }
Loading length of array costs gas. Therefore, the length should be cached, if the length of the array doesn't change inside the loop. Furthermore, there is no need to assign the initial value 0. This costs extra gas.
Recommended implementation:
uint length = arr.length; for (uint i; i < length; i++) { //Operations not effecting the length of the array. }
By doing so the length is only loaded once rather than loading it as many times as iterations (Therefore, less gas is spent).
./TRIBERagequit.sol:165: for (uint256 i = 0; i < proof.length; i++) {
#0 - pauliax
2021-12-10T17:42:25Z
Valid optimization, mentioning both cases, default initialization, and caching length.