Platform: Code4rena
Start Date: 25/11/2021
Pot Size: $80,000 USDC
Total HM: 35
Participants: 32
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 27
Id: 59
League: ETH
Rank: 11/32
Findings: 5
Award: $2,139.80
🌟 Selected for report: 6
🚀 Solo Findings: 1
ScopeLift
TBD how bad the impact is
The docs mention that the notSameBlock
modifier (and associated _notSameBlock()
method) is used to guard against reentrancy. However, users can still call a method twice in a single transaction by using transferring assets/positions between two accounts they control, and re-entering with the other account
N/A
Change notSameBlock
to a standard reentrancy guard such as this one from Solmate
abstract contract ReentrancyGuard { uint256 private reentrancyStatus = 1; modifier nonReentrant() { require(reentrancyStatus == 1, "REENTRANCY"); reentrancyStatus = 2; _; reentrancyStatus = 1; } }
#0 - 0xScotch
2021-12-08T11:40:08Z
#195
#1 - GalloDaSballo
2022-01-09T23:34:36Z
In lack of POC this goes down to medium
#2 - GalloDaSballo
2022-01-09T23:34:43Z
Duplicate of #195
🌟 Selected for report: ScopeLift
1103.7569 USDC - $1,103.76
ScopeLift
The internal _withdraw
method does not follow the checks-effects-interactions pattern. A malicious token, or one that implemented transfer hooks, could re-enter the public calling function (such as withdraw()
) before proper internal accounting was completed. Because the earned
function looks up the _userWithdrawn
mapping, which is not yet updated when the transfer occurs, it would be possible for a malicious contract to re-enter _withdraw
repeatedly and drain the pool.
N/A
N/A
The internal accounting should be done before the transfer occurs:
function _withdraw(address account, uint256 amountReward, address to) internal { _userWithdrawn[account] += amountReward; _globalWithdrawn += amountReward;f rewardToken.safeTransfer(to, amountReward); emit Withdraw(account, amountReward, to); }
#0 - GalloDaSballo
2022-01-09T23:41:55Z
The warden identified a re-entrancy vulnerability that, given the right token would allow to drain the entirety of the contract.
Tokens with hooks (ERC777 and ERC677) would allow to exploit the contract and drain it in it's entirety.
This is a very serious vulnerability. However it can happen exclusively on a malicious or a token with hooks, as such (while I recommend the sponsor to mitigate by following recommendation by the warden), the attack can be completely prevented by using a token without hooks.
For that reason I'll rate the finding of medium severity (as it requires external conditions)
496.6906 USDC - $496.69
ScopeLift
On lines 85 and 101, ETH is transferred using a .call
to an address provided as an input, but there is no verification that the call call succeeded. This can result in a call to emergencyWithdrawGAS
or partialWithdrawGAS
appearing successful but in reality it failed. This can happen when the provided destination
address is a contract that cannot receive ETH, or if the amount
provided is larger than the contract's balance
Enter the following in remix, deploy the Receiver
contract, and send 1 ETH when deploying the Permissions
contract. Call emergencyWithdrawGAS
with the receiver address and you'll see it reverts. This would not be caught in the current code
pragma solidity ^0.8.0; contract Receivier{} contract Permissions { constructor() payable {} function emergencyWithdrawGAS(address payable destination) external { (bool ok, ) = destination.call{value: address(this).balance}(''); require(ok, "call failed"); } }
Remix
In emergencyWithdrawGAS
:
- destination.call{value: address(this).balance}(''); + (bool ok, ) = destination.call{value: address(this).balance}(''); + require(ok, "call failed");
And similar for partialWithdrawGAS
#0 - GalloDaSballo
2022-01-22T15:33:17Z
Agree with the finding, I believe if a developer were to not use safeTransfer we'd rate as medium, so while I believe the impact to be minimal (no composability), I'll keep the severity to medium
15.2616 USDC - $15.26
ScopeLift
Gas can be saved inside the addLiquidity
function by avoiding the usage of safe math where the overflow condition has already been checked via conditional.
N/A
N/A
Update amounts used in transfer calls to use unchecked subtraction:
if (maltUsed < maltBalance) { malt.safeTransfer(msg.sender, maltBalance - maltUsed); } if (rewardUsed < rewardBalance) { rewardToken.safeTransfer(msg.sender, rewardBalance - rewardUsed); }
#0 - 0xScotch
2021-12-08T18:33:25Z
#307
#1 - GalloDaSballo
2022-01-16T15:06:17Z
Duplicate of #307
🌟 Selected for report: ScopeLift
56.5245 USDC - $56.52
ScopeLift
Inside provideReinvest
and _bondAccount
gas can be saved by using the standard transfer method on the Malt token, since we know its implementation is correct and will return true/false.
N/A
N/A
Replace malt.safeTransfer(address(dexHandler), balance);
with something like:
require(malt.transfer(address(dexHandler), balance), 'malt transfer failed');
#0 - GalloDaSballo
2022-01-16T15:05:49Z
Interesting suggested optimization from the warden, this will factually save the cost of storing the return data on memory as well as the if. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/25eeb80b188876b951e592a810785173495097fc/contracts/token/ERC20/utils/SafeERC20.sol#L94
Probably would save 3 + 10 + 3 gas
Personally like the readability from safeTransfer, but the finding is valid
🌟 Selected for report: ScopeLift
56.5245 USDC - $56.52
ScopeLift
Calldata costs 16 gas per non-zero byte and 4 gas per zero byte. Unused function parameters take up at least 32 bytes, which means unused parameters can cost between 128 and 512 gas
N/A
solc
Remove the unused function parameters identified by the Solidity compiler. For examples like AbstractTransferVerification
, the solidity compiler is wrong because it doesn't know you plan to override this function declaration. Instead, AbstractTransferVerification
could be an interface without a function definition
contracts/AbstractTransferVerification.sol:9:27: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function verifyTransfer(address from, address to, uint256 amount) public view virtual returns (bool, string memory) { ^----------^ contracts/AbstractTransferVerification.sol:9:41: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function verifyTransfer(address from, address to, uint256 amount) public view virtual returns (bool, string memory) { ^--------^ contracts/AbstractTransferVerification.sol:9:53: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function verifyTransfer(address from, address to, uint256 amount) public view virtual returns (bool, string memory) { ^------------^ contracts/Auction.sol:605:7: Warning: Unused local variable. uint256 avgMaltPrice, ^------------------^ contracts/AuctionParticipant.sol:127:38: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function _handleRewardDistribution(uint256 rewarded) virtual internal { ^--------------^ contracts/AuctionPool.sol:136:27: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function _afterWithdraw(address account, uint256 amount) override internal { ^-------------^ contracts/DexHandlers/UniswapHandler.sol:308:5: Warning: Unused local variable. address buyer; ^-----------^ contracts/MaltDataLab.sol:159:5: Warning: Unused local variable. uint256 rewardDecimals = rewardToken.decimals(); ^--------------------^ contracts/MiningService.sol:26:5: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. address _rewardToken, ^------------------^ contracts/PoolTransferVerification.sol:45:53: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function verifyTransfer(address from, address to, uint256 amount) ^------------^ contracts/RewardSystem/RewardOverflowPool.sol:67:38: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function _handleRewardDistribution(uint256 amount) override internal {
#0 - GalloDaSballo
2022-01-16T15:33:58Z
Agree with the finding, removing unused parameters will save some gas
🌟 Selected for report: ScopeLift
56.5245 USDC - $56.52
ScopeLift
There are four functions that can have stricter function mutability declarations. Using a stricter declaration can help the compiler save gas when it knows whether reads and writes will occur in a function
N/A
solc
Implement the changes suggested by the Solidity compiler. For examples like AbstractTransferVerification
, the solidity compiler is wrong because it doesn't know you plan to override this function declaration. Instead, AbstractTransferVerification
could be an interface without a function definition
contracts/AbstractTransferVerification.sol:9:3: Warning: Function state mutability can be restricted to pure function verifyTransfer(address from, address to, uint256 amount) public view virtual returns (bool, string memory) { ^ (Relevant source part starts here and spans across multiple lines). contracts/AuctionEscapeHatch.sol:168:3: Warning: Function state mutability can be restricted to view function _calculateMaltRequiredForExit(uint256 _auctionId, uint256 amount) internal returns(uint256) { ^ (Relevant source part starts here and spans across multiple lines). contracts/AuctionParticipant.sol:127:3: Warning: Function state mutability can be restricted to pure function _handleRewardDistribution(uint256 rewarded) virtual internal { ^ (Relevant source part starts here and spans across multiple lines). contracts/MaltDataLab.sol:202:3: Warning: Function state mutability can be restricted to pure function _normalizedPrice( ^ (Relevant source part starts here and spans across multiple lines).
#0 - GalloDaSballo
2022-01-16T15:34:54Z
Agree with restricting to pure, esp when the compiler flags it up. The first finding is for a test file so it's not that important
🌟 Selected for report: ScopeLift
56.5245 USDC - $56.52
ScopeLift
Gas Optimization
Instantiating an array of length n is better than push(0)
n times and saves 20k gas in tests.
change the initializer
## Saves ~20,000 gas on initialize diff --git a/src/contracts/AuctionBurnReserveSkew.sol b/src/contracts/AuctionBurnReserveSkew.sol index 4ed6fa6..87d5959 100644 --- a/src/contracts/AuctionBurnReserveSkew.sol +++ b/src/contracts/AuctionBurnReserveSkew.sol @@ -51,9 +51,7 @@ contract AuctionBurnReserveSkew is Initializable, Permissions { auction = IAuction(_auction); auctionAverageLookback = _period; - for (uint i = 0; i < _period; i++) { - pegObservations.push(0); - } + pegObservations = new uint256[](_period); } function consult(uint256 excess) public view returns (uint256) {
#0 - GalloDaSballo
2022-01-16T15:37:40Z
I agree with the optimization