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: 1/21
Findings: 2
Award: $6,967.18
🌟 Selected for report: 8
🚀 Solo Findings: 0
13.4097 USDC - $13.41
WatchPug
require(msg.sender != address(this), "????");
exchange()
can not be called from the contract itself, therefore require(msg.sender != address(this)
is redundant.
#0 - pauliax
2021-12-10T15:51:23Z
Valid optimization.
13.4097 USDC - $13.41
WatchPug
function giveTo(address target, uint256 amount) internal { bool check = token1.transferFrom(address(this), target, amount); require(check, "erc20 transfer failed"); }
In PegExchanger.sol#giveTo()
, transferFrom
is used to transfer funds from address(this)
to the target. Since transferFrom
includes unnecessary check of if (spender != src && spenderAllowance != uint96(-1)) {..}
, it's more gas efficient to use transfer
instead.
#0 - elee1766
2021-12-06T04:37:48Z
#104
#1 - pauliax
2021-12-10T16:01:22Z
A duplicate of #104
🌟 Selected for report: WatchPug
Also found by: Meta0xNull
33.1103 USDC - $33.11
WatchPug
Every reason string takes at least 32 bytes.
Use short reason strings that fits in 32 bytes or it will become more expensive.
Instances include:
require( msg.sender == party0Timelock, "Only the timelock for party 0 may call this function" );
require( msg.sender == party1Timelock, "Only the timelock for party 1 may call this function" );
#0 - pauliax
2021-12-10T17:26:48Z
Valid optimization.
🌟 Selected for report: WatchPug
73.5785 USDC - $73.58
WatchPug
function requery() public returns (int256) { ( uint256 _pcvValue, // pcv value uint256 _userFei, // user fei int256 newProtocolEquity, bool validity ) = oracle.pcvStats(); if (minProtocolEquity == 0) { if (init == false) { minProtocolEquity = newProtocolEquity; recalculate(); init = true; return minProtocolEquity; } } if (minProtocolEquity > newProtocolEquity) { minProtocolEquity = newProtocolEquity; recalculate(); return minProtocolEquity; } return minProtocolEquity; }
Change to else if
can save gas:
function requery() public returns (int256) { ( uint256 _pcvValue, // pcv value uint256 _userFei, // user fei int256 newProtocolEquity, bool validity ) = oracle.pcvStats(); if (minProtocolEquity == 0) { if (init == false) { minProtocolEquity = newProtocolEquity; recalculate(); init = true; return minProtocolEquity; } } else if (minProtocolEquity > newProtocolEquity) { minProtocolEquity = newProtocolEquity; recalculate(); return minProtocolEquity; } return minProtocolEquity; }
#0 - pauliax
2021-12-10T17:40:45Z
This is a valid optimization, not a duplicate.
🌟 Selected for report: 0x0x0x
Also found by: Meta0xNull, WatchPug
19.8662 USDC - $19.87
WatchPug
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Instances include:
TRIBERagequit.sol#processProof()
#0 - elee1766
2021-12-06T04:34:58Z
#134
#1 - pauliax
2021-12-10T17:43:56Z
A duplicate of #134
WatchPug
uint256 public preMergeCirculatingTribe = 711206739862133000000000000;
Some storage variables include preMergeCirculatingTribe
will not never be changed and they should not be.
Changing them to constant
can save gas.
#0 - elee1766
2021-12-06T03:38:40Z
#147
#1 - pauliax
2021-12-10T17:49:44Z
A duplicate of #147
WatchPug
Redundant code increase contract size and gas usage at deployment.
Can be changed to require(isEnabled(), "...")
.
require( isEnabled() == true, "Contract must be enabled before admin functions called" );
Other exmaples include:
function ngmi( uint256 multiplier, uint256 key, bytes32[] memory merkleProof ) public { require(isExpired() == false, "Redemption period is over"); require(isEnabled() == true, "Proposals are not both passed"); require(minProtocolEquity > 0, "no equity"); address thisSender = msg.sender; require( verifyClaim(thisSender, key, merkleProof) == true, "invalid proof" );
function exchange(uint256 multiplier) public { require(isExpired() == false, "Redemption period is over"); require(isEnabled() == true, "Proposals are not both passed"); require(msg.sender != address(this), "????"); uint256 token0TakenTotal = token0InBase * multiplier; uint256 token1GivenTotal = token1OutBase * multiplier; takeFrom(msg.sender, token0TakenTotal); giveTo(msg.sender, token1GivenTotal); emit Exchange(msg.sender, token0TakenTotal, token1GivenTotal); }
#0 - elee1766
2021-12-06T04:41:05Z
#160
#1 - pauliax
2021-12-10T18:30:55Z
A duplicate of #160
19.8662 USDC - $19.87
WatchPug
For the arithmetic operations that will never over/underflow, using the unchecked directive (Solidity v0.8 has default overflow/underflow checks) can save some gas from the unnecessary internal over/underflow checks.
For example:
function _startCountdown() internal { if (expirationBlock == 0) { expirationBlock = block.number + 6400 * 3; // approx. 3 days in blocks } }
block.number + 6400 * 3
will never overflow.
#0 - elee1766
2021-12-06T04:29:51Z
#124
#1 - pauliax
2021-12-10T19:23:08Z
A duplicate of #124
WatchPug
uint256 public expirationBlock = 0;
Setting uint256
variables to 0
is redundant as they default to 0
.
Other examples include:
#0 - elee1766
2021-12-06T04:39:49Z
#157
#1 - pauliax
2021-12-11T09:56:54Z
A duplicate of #157
WatchPug
function recalculate() public returns (uint256) { if (minProtocolEquity > 0) { token0InBase = scalar; token1OutBase = (scalar * uint256(minProtocolEquity)) / preMergeCirculatingTribe; } else { token1OutBase = 0; } return token1OutBase; }
Given that everytime minProtocolEquity
got updated, recalculate()
will be called internaly by requery()
, there is no need to make recalculate()
a public
method.
Calling recalculate()
from the outside will be a waste of gas.
#0 - elee1766
2021-12-06T04:36:22Z
#26
#1 - pauliax
2021-12-11T11:28:35Z
A duplicate of #38
🌟 Selected for report: WatchPug
3272.101 USDC - $3,272.10
WatchPug
Per to the doc:
- Mints more tribe
- Send tribe to the PegExchanger
The tribe tokens used by PegExchanger.sol
for exchange are sent from the tribe gov directly to the wallet balance of the PegExchanger
contract.
It most certainly will leftover some of the unused tribe tokens, especially when isExpired()
. There is no way for the admin or anyone else to retrieve these leftover tribe tokens.
Consider changing to: mint the tribe tokens to the tribe gov or a special proposed contract that holds the tribe tokens, and approve a certain amount to the PegExchange
contract.
Change the giveTo
function to:
function giveTo(address target, uint256 amount) internal { bool check = token1.transferFrom(TRIBE_GOV, target, amount); require(check, "erc20 transfer failed"); }
When the exchange ends, the allowance can be revoked.
#0 - elee1766
2021-12-06T03:50:09Z
already known, similar to #94 but much less of an issue
frozen tribe does not actually pose a real security risk if the contract is disabled, one may simply deem it out of circulation. Adding another contract would only make the system even more complicated, therefore reducing any perceived security benefit.
#1 - pauliax
2021-12-11T12:39:20Z
Similar to #94. I don't think this deserves a severity of high, because unclaimed tokens are supposed to be out of circulation and expirationBlock can be extended (#63).
33.1103 USDC - $33.11
WatchPug
Here are some examples that the code style does not follow the best practices:
/// @notice function which ultimately transfers the RGT /// @param target the address of which to transfer RGT from /// @param amount the amount of RGT to transfer from the target address function takeFrom(address target, uint256 amount) internal { bool check = token0.transferFrom(target, address(this), amount); require(check, "erc20 transfer failed"); } /// @notice function which ultimately transfers the TRIBE /// @param target the address of which to transfer TRIBE to /// @param amount the amount of TRIBE to transfer to the target address function giveTo(address target, uint256 amount) internal { bool check = token1.transferFrom(address(this), target, amount); require(check, "erc20 transfer failed"); }
#0 - elee1766
2021-12-06T04:34:18Z
does this save gas? if not it's not an issue
#1 - pauliax
2021-12-11T12:18:37Z
A duplicate of #26
33.1103 USDC - $33.11
WatchPug
Using ++i
is more gas efficient than i++
, especially in a loop.
We suggest to use unchecked {++i} to even better gas performance. (for >= 0.8)
For example:
for (uint256 i = 0; i < proof.length; i++) { bytes32 proofElement = proof[i]; if (computedHash <= proofElement) { computedHash = keccak256( abi.encodePacked(computedHash, proofElement) ); } else { computedHash = keccak256( abi.encodePacked(proofElement, computedHash) ); } }
#0 - pauliax
2021-12-11T10:37:09Z
Valid, similar to #134 and #124
🌟 Selected for report: Meta0xNull
Also found by: WatchPug
33.1103 USDC - $33.11
WatchPug
expirationBlock = block.number + 6400 * 3; // approx. 3 days in blocks
Change to:
expirationBlock = block.number + 19200; // approx. 3 days in blocks
#0 - elee1766
2021-12-06T04:31:01Z
#84
#1 - pauliax
2021-12-11T10:48:15Z
Similar to #45
19.8662 USDC - $19.87
WatchPug
function verifyClaim( address claimer, uint256 key, bytes32[] memory merkleProof ) private view returns (bool) { bytes32 leaf = keccak256(abi.encodePacked(claimer, key)); return verifyProof(merkleProof, merkleRoot, leaf); }
leaf
is unnecessary as it's being used only once. Can be changed to:
function verifyClaim( address claimer, uint256 key, bytes32[] memory merkleProof ) private view returns (bool) { return verifyProof(merkleProof, merkleRoot, keccak256(abi.encodePacked(claimer, key))); }
function ngmi( uint256 multiplier, uint256 key, bytes32[] memory merkleProof ) public { require(isExpired() == false, "Redemption period is over"); require(isEnabled() == true, "Proposals are not both passed"); require(minProtocolEquity > 0, "no equity"); address thisSender = msg.sender; require( verifyClaim(thisSender, key, merkleProof) == true, "invalid proof" ); require( (claimed[thisSender] + multiplier) <= key, "already ragequit all you tokens" ); claimed[thisSender] = claimed[thisSender] + multiplier; uint256 token0TakenTotal = token0InBase * multiplier; uint256 token1GivenTotal = token1OutBase * multiplier; takeFrom(thisSender, token0TakenTotal); giveTo(thisSender, token1GivenTotal); emit Exchange(thisSender, token0TakenTotal, token1GivenTotal); }
thisSender
is unnecessary, it can be replaced with msg.sender
.
#0 - pauliax
2021-12-11T11:59:58Z
Valid optimization.
33.1103 USDC - $33.11
WatchPug
uint256 public token0InBase = 2**256 - 1;
Change to:
uint256 public token0InBase = type(uint256).max;
#0 - elee1766
2021-12-06T04:30:11Z
#100
#1 - pauliax
2021-12-11T10:48:58Z
Similar to #100
19.8662 USDC - $19.87
WatchPug
For the storage variables that will be accessed multiple times, cache and read from the stack can save ~100 gas from each extra read (SLOAD
after Berlin).
For example:
function requery() public returns (int256) { ( uint256 _pcvValue, // pcv value uint256 _userFei, // user fei int256 newProtocolEquity, bool validity ) = oracle.pcvStats(); if (minProtocolEquity == 0) { if (init == false) { minProtocolEquity = newProtocolEquity; recalculate(); init = true; return minProtocolEquity; } } if (minProtocolEquity > newProtocolEquity) { minProtocolEquity = newProtocolEquity; recalculate(); return minProtocolEquity; } return minProtocolEquity; }
minProtocolEquity
can be cached.
Change to:
function requery() public returns (int256) { ( uint256 _pcvValue, // pcv value uint256 _userFei, // user fei int256 newProtocolEquity, bool validity ) = oracle.pcvStats(); uint256 _minProtocolEquity = minProtocolEquity; if (_minProtocolEquity == 0) { if (init == false) { minProtocolEquity = newProtocolEquity; recalculate(); init = true; return minProtocolEquity; } } if (_minProtocolEquity > newProtocolEquity) { minProtocolEquity = newProtocolEquity; recalculate(); return minProtocolEquity; } return _minProtocolEquity; }
#0 - pauliax
2021-12-11T12:04:30Z
A duplicate of #25
🌟 Selected for report: 0x0x0x
Also found by: GeekyLumberjack, WatchPug
19.8662 USDC - $19.87
WatchPug
function recalculate() public returns (uint256) { if (minProtocolEquity > 0) { token0InBase = scalar; token1OutBase = (scalar * uint256(minProtocolEquity)) / preMergeCirculatingTribe; } else { token1OutBase = 0; } return token1OutBase; }
The returns of recalculate()
are unused, so that they can be removed to avoid unnecessary storage read of token1OutBase
and save some gas.
Change to:
function recalculate() public { if (minProtocolEquity > 0) { token0InBase = scalar; token1OutBase = (scalar * uint256(minProtocolEquity)) / preMergeCirculatingTribe; } else { token1OutBase = 0; } }
#0 - pauliax
2021-12-11T12:08:18Z
A duplicate of #138
🌟 Selected for report: WatchPug
3272.101 USDC - $3,272.10
WatchPug
The current implementation requires a minter role of the FEI token:
function giveTo(address target, uint256 amount) internal { token1.mint(target, amount); }
We believe it's unnecessary and can potentially pose a security risk. For example, if merkleRoot
is somehow being malformed or tempered. The TribeRagequit
can mint a much bigger amount of FEI
token than expected and cause systematic risk affecting the whole FEI protocol.
Otoh, if TribeRagequit
is going to be deployed as an upgradable contract, this change can also help prevent the deployer of TribeRagequit
from abusing the minter role.
Change from:
To:
TribeRagequit
contract;TribeRagequit.isExpired()
.In addition to the changes above, a new function to burn the unused FEI tokens when TribeRagequit.isExpired()
can be added.
#0 - elee1766
2021-12-06T03:48:07Z
ack'd
good point, but would require major changes around current proposal
should be brought up as a footnote, contracts will change if they decide to revise the proposal
#1 - pauliax
2021-12-11T12:31:24Z
A valid concern, even though the issue is very theoretical because the contracts are not supposed to be upgradeable and an invalid merkle root would ruin everything anyway.
WatchPug
function exchange(uint256 multiplier) public { require(isExpired() == false, "Redemption period is over"); require(isEnabled() == true, "Proposals are not both passed"); require(msg.sender != address(this), "????"); uint256 token0TakenTotal = token0InBase * multiplier; uint256 token1GivenTotal = token1OutBase * multiplier; takeFrom(msg.sender, token0TakenTotal); giveTo(msg.sender, token1GivenTotal); emit Exchange(msg.sender, token0TakenTotal, token1GivenTotal); }
function takeFrom(address target, uint256 amount) internal { bool check = token0.transferFrom(target, address(this), amount); require(check, "erc20 transfer failed"); }
function giveTo(address target, uint256 amount) internal { bool check = token1.transferFrom(address(this), target, amount); require(check, "erc20 transfer failed"); }
takeFrom()
and giveTo()
are unnecessary as they are used only once. Therefore they can be inlined in exchange()
to make the code simpler and save gas.
Change to:
function exchange(uint256 multiplier) public { require(isExpired() == false, "Redemption period is over"); require(isEnabled() == true, "Proposals are not both passed"); require(msg.sender != address(this), "????"); uint256 token0TakenTotal = token0InBase * multiplier; uint256 token1GivenTotal = token1OutBase * multiplier; require(token0.transferFrom(msg.sender, address(this), token0TakenTotal), "erc20 transfer failed"); require(token1.transfer(msg.sender, token1GivenTotal), "erc20 transfer failed"); emit Exchange(msg.sender, token0TakenTotal, token1GivenTotal); }
#0 - elee1766
2021-12-06T03:56:24Z
#120
#1 - pauliax
2021-12-11T12:13:02Z
This is a different issue, suggesting refactoring internal functions that are only used once. This will reduce the readability but technically is a valid gas optimization.