Platform: Code4rena
Start Date: 08/12/2021
Pot Size: $30,000 ETH
Total HM: 12
Participants: 26
Period: 3 days
Judge: leastwood
Total Solo HM: 9
Id: 65
League: ETH
Rank: 3/26
Findings: 4
Award: $4,170.46
🌟 Selected for report: 16
🚀 Solo Findings: 1
🌟 Selected for report: TomFrenchBlockchain
Also found by: WatchPug
690.8489 USDC - $690.85
WatchPug
According to the newRatio
formula in settleAuction()
, the maximum value of newRatio
is factory.auctionMultiplier() * basket.ibRatio()
.
However, since there is no validation for the value of minIbRatio
when setting it, if the publisher publishes a newIndex with minIbRatio
larger than factory.auctionMultiplier() * basket.ibRatio()
, settleAuction()
will always fail.
uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondBlock - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b;
function publishNewIndex(address[] memory _tokens, uint256[] memory _weights, uint256 _minIbRatio) onlyPublisher public override { validateWeights(_tokens, _weights); if (pendingWeights.pending) { require(block.timestamp >= pendingWeights.timestamp + TIMELOCK_DURATION); if (auction.auctionOngoing() == false) { auction.startAuction(); emit PublishedNewIndex(publisher); } else if (auction.hasBonded()) { } else { auction.killAuction(); pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.timestamp = block.timestamp; pendingWeights.minIbRatio = _minIbRatio; } } else { pendingWeights.pending = true; pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.timestamp = block.timestamp; pendingWeights.minIbRatio = _minIbRatio; emit NewIndexSubmitted(); } }
bondPercentDiv
= 400basketToken.totalSupply
= 40,000factory.auctionMultiplier
= 2factory.auctionDecrement
= 10,000basket.ibRatio
= 1e18publishNewIndex()
with _minIbRatio
= 2e18
bondForRebalance()
just after 1 block, paid 100
basketTokensettleAuction()
, it will always fail because newRatio < minIbRatio
bondBurn()
after 24 hrs, and Alice's 100
basketToken will be burned.Change to:
function publishNewIndex(address[] memory _tokens, uint256[] memory _weights, uint256 _minIbRatio) onlyPublisher public override { validateWeights(_tokens, _weights); require(_minIbRatio < factory.auctionMultiplier() * ibRatio, "!_minIbRatio"); if (pendingWeights.pending) { require(block.timestamp >= pendingWeights.timestamp + TIMELOCK_DURATION); if (auction.auctionOngoing() == false) { auction.startAuction(); emit PublishedNewIndex(publisher); } else if (auction.hasBonded()) { } else { auction.killAuction(); pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.timestamp = block.timestamp; pendingWeights.minIbRatio = _minIbRatio; } } else { pendingWeights.pending = true; pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.timestamp = block.timestamp; pendingWeights.minIbRatio = _minIbRatio; emit NewIndexSubmitted(); } }
#0 - frank-beard
2022-02-22T19:20:27Z
while this could technically happen, it should be up to the auction rebalancer to make sure they can actually settle the auction whether that's how much capital is required, and possible issues with the new weights. i would consider this a low risk issue.
#1 - 0xleastwood
2022-03-27T02:21:37Z
I agree, I think it is expected that the auction rebalancer will check minIbRatio
before calling bondForRebalance()
. However, I actually think this is a duplicate of #53 as it also describes how an auction rebalancer can have their tokens burnt by a malicious publisher.
🌟 Selected for report: WatchPug
1535.2197 USDC - $1,535.22
WatchPug
uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondBlock - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; (address[] memory pendingTokens, uint256[] memory pendingWeights, uint256 minIbRatio) = basket.getPendingWeights(); require(newRatio >= minIbRatio);
In the current implementation, newRatio
is calculated and compared with minIbRatio
in settleAuction()
.
However, if newRatio
is less than minIbRatio
, settleAuction()
will always fail and there is no way for the bonder to cancel and get a refund.
Given:
bondPercentDiv
= 400basketToken.totalSupply
= 40,000factory.auctionMultiplier
= 2factory.auctionDecrement
= 10,000basket.ibRatio
= 1e18endingWeights.minIbRatio
= 1.9 * 1e18bondForRebalance()
2,000
blocks after the auction started, paid 100
basketToken for the bond;settleAuction()
, it will always fail because newRatio < minIbRatio
;bondBurn()
one day after, 100
basketToken from Alice will been burned.Move the minIbRatio
check to bondForRebalance()
:
function bondForRebalance() public override { require(auctionOngoing); require(!hasBonded); bondTimestamp = block.timestamp; bondBlock = block.number; uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondBlock - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; (address[] memory pendingTokens, uint256[] memory pendingWeights, uint256 minIbRatio) = basket.getPendingWeights(); require(newRatio >= minIbRatio); IERC20 basketToken = IERC20(address(basket)); bondAmount = basketToken.totalSupply() / factory.bondPercentDiv(); basketToken.safeTransferFrom(msg.sender, address(this), bondAmount); hasBonded = true; auctionBonder = msg.sender; emit Bonded(msg.sender, bondAmount); }
#0 - 0xleastwood
2022-03-27T02:14:54Z
While this issue is correct and I think this is a safer way to handle the require(newRatio >= minIbRatio)
check, there are a few assumptions that are made. For example, it is assumed that the user bonds their tokens without checking minIbRatio
and a publisher is able to maliciously update minIbRatio
which must first go through timelock. Based on this, I'm more inclined to downgrade this to medium
severity as I think this more accurately reflects the threat model.
🌟 Selected for report: WatchPug
WatchPug
When changing licenseFee from 0
, there are no methods to cancel pending changes. It can only be set to a none 0
value. Otherwise changeLicenseFee()
will revert on the assertion and check of: newLicenseFee != licenseFee
and pendingLicenseFee.licenseFee != 0
.
As a result, wrong changes may not be able to get canceled.
function changeLicenseFee(uint256 newLicenseFee) onlyPublisher public override { require(newLicenseFee >= factory.minLicenseFee() && newLicenseFee != licenseFee); if (pendingLicenseFee.licenseFee != 0 && pendingLicenseFee.licenseFee == newLicenseFee) { require(block.timestamp >= pendingLicenseFee.timestamp + TIMELOCK_DURATION); licenseFee = newLicenseFee; pendingLicenseFee.licenseFee = 0; emit ChangedLicenseFee(licenseFee); } else { pendingLicenseFee.licenseFee = newLicenseFee; pendingLicenseFee.timestamp = block.timestamp; emit NewLicenseFeeSubmitted(newLicenseFee); } }
Given:
factory.minLicenseFee
= 0
licenseFee
= 0
Consider removing the requirement of newLicenseFee != licenseFee
at L178, allowing pendingLicenseFee to be canceled by setting back to the current value.
#0 - frank-beard
2022-02-23T18:16:23Z
It is intended for a basket to always have a minLicenseFee, so the case of changing that value from 0 should not happen.
#1 - 0xleastwood
2022-03-27T05:45:12Z
I disagree, if the basket is initialised with licenseFee
set to zero due to the minimum license fee not being correctly configured, then this issue is prevalent. I recommend that you ensure the minimum license fee is greater than zero.
🌟 Selected for report: WatchPug
511.7399 USDC - $511.74
WatchPug
Given that Basket
is deployed as a proxied contract, it should use the Upgradeable variant of OpenZeppelin Contracts.
function createBasket(uint256 idNumber) external override nonReentrant returns (IBasket) { Proposal memory bProposal = _proposals[idNumber]; require(bProposal.basket == address(0)); IAuction newAuction = IAuction(Clones.clone(address(auctionImpl))); IBasket newBasket = IBasket(Clones.clone(address(basketImpl))); _proposals[idNumber].basket = address(newBasket); newAuction.initialize(address(newBasket), address(this)); newBasket.initialize(bProposal, newAuction); for (uint256 i = 0; i < bProposal.weights.length; i++) { IERC20 token = IERC20(bProposal.tokens[i]); token.safeTransferFrom(msg.sender, address(this), bProposal.weights[i]); token.safeApprove(address(newBasket), bProposal.weights[i]); } newBasket.mintTo(BASE, msg.sender); emit BasketCreated(address(newBasket)); return newBasket; }
Otherwise, the constructor functions of Basket
's parent contract which may change storage at deploy time, won't work for clone instances.
The effect may be different for different OpenZeppelin libraries.
Take ReentrancyGuard
for example, the code inside ReentrancyGuard.sol#constructor
won't work, should use ReentrancyGuardUpgradeable.sol
instead:
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Basket is IBasket, ERC20Upgradeable, ReentrancyGuard {
function initialize(IFactory.Proposal memory proposal, IAuction auction_) external override { require(address(factory) == address(0)); require(!initialized); publisher = proposal.proposer; licenseFee = proposal.licenseFee; factory = IFactory(msg.sender); auction = auction_; ibRatio = BASE; tokens = proposal.tokens; weights = proposal.weights; maxSupply = proposal.maxSupply; approveUnderlying(address(auction)); __ERC20_init(proposal.tokenName, proposal.tokenSymbol); initialized = true; }
Change to:
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
contract Basket is IBasket, ERC20Upgradeable, ReentrancyGuardUpgradeable {
function initialize(IFactory.Proposal memory proposal, IAuction auction_) external initializer override { require(address(factory) == address(0)); publisher = proposal.proposer; licenseFee = proposal.licenseFee; factory = IFactory(msg.sender); auction = auction_; ibRatio = BASE; tokens = proposal.tokens; weights = proposal.weights; maxSupply = proposal.maxSupply; approveUnderlying(address(auction)); __ERC20_init(proposal.tokenName, proposal.tokenSymbol); __ReentrancyGuard_init(); }
🌟 Selected for report: WatchPug
511.7399 USDC - $511.74
WatchPug
function setMinLicenseFee(uint256 newMinLicenseFee) public override onlyOwner { minLicenseFee = newMinLicenseFee; }
Factory
allows minLicenseFee
to be 0
.
But in the current implementation, publisher cannot set licenseFee
to 0
with Basket#changeLicenseFee()
.
function changeLicenseFee(uint256 newLicenseFee) onlyPublisher public override { require(newLicenseFee >= factory.minLicenseFee() && newLicenseFee != licenseFee); if (pendingLicenseFee.licenseFee != 0 && pendingLicenseFee.licenseFee == newLicenseFee) { require(block.timestamp >= pendingLicenseFee.timestamp + TIMELOCK_DURATION); licenseFee = newLicenseFee; pendingLicenseFee.licenseFee = 0; emit ChangedLicenseFee(licenseFee); } else { pendingLicenseFee.licenseFee = newLicenseFee; pendingLicenseFee.timestamp = block.timestamp; emit NewLicenseFeeSubmitted(newLicenseFee); } }
The check if (pendingLicenseFee.licenseFee != 0 && ...) {
at L179 only allows licenseFee
to be none 0 values for pendingLicenseFee.licenseFee
.
Consider adding pendingLicenseFee.pending
to check if there is a pending licenseFee
.
#0 - frank-beard
2022-02-23T18:17:12Z
while it is allowable for minLicenseFee to be 0, it is intended for baskets to always have some amount of license fee on them.
🌟 Selected for report: 0x0x0x
Also found by: Jujic, Meta0xNull, WatchPug, pmerkleplant, rishabh
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:
for (uint256 x = 0; x < tokenList.length; x++) {
for (uint256 i = 0; i < weights.length; i++) {
for (uint256 i = 0; i < weights.length; i++) {
for (uint256 i = 0; i < weights.length; i++) {
for (uint256 i = 0; i < pendingWeights.length; i++) {
#0 - 0xleastwood
2022-03-27T10:32:10Z
Duplicate of #140
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 example:
for (uint256 i = 0; i < bProposal.weights.length; i++) {
for (uint256 i = 0; i < bountyIds.length; i++) {
for (uint256 i = 0; i < weights.length; i++) {
WatchPug
function handleFees(uint256 startSupply) private { if (lastFee == 0) { lastFee = block.timestamp; } else if (startSupply == 0) { return; } else { uint256 timeDiff = (block.timestamp - lastFee); uint256 feePct = timeDiff * licenseFee / ONE_YEAR; uint256 fee = startSupply * feePct / (BASE - feePct); _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE); lastFee = block.timestamp; uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; emit NewIBRatio(ibRatio); } }
timeDiff
in Basket.sol#handleFees()
can be 0
when there are multiple mint
and burn
transactions in the same block. Check if timeDiff > 0
can save gas from unnecessary code execution like mint
with amount 0
.
Change to:
function handleFees(uint256 startSupply) private { if (lastFee == 0) { lastFee = block.timestamp; } else if (startSupply == 0) { return; } else { uint256 timeDiff = (block.timestamp - lastFee); if (timeDiff == 0) return; uint256 feePct = timeDiff * licenseFee / ONE_YEAR; uint256 fee = startSupply * feePct / (BASE - feePct); _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE); lastFee = block.timestamp; uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; emit NewIBRatio(ibRatio); } }
🌟 Selected for report: WatchPug
Also found by: TomFrenchBlockchain
WatchPug
Every call to an external contract costs a decent amount of gas. For optimization of gas usage, external call results should be cached if they are being used for more than one time.
For example:
factory.ownerSplit()
in Basket#handleFees()
_mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE);
🌟 Selected for report: GiveMeTestEther
Also found by: Jujic, TomFrenchBlockchain, WatchPug, gzeon, kenzo, sirhashalot, ye0lde
WatchPug
Setting uint256
variables to 0
is redundant as they default to 0
.
uint256 public override ownerSplit;
constructor (IAuction _auctionImpl, IBasket _basketImpl) { auctionImpl = _auctionImpl; basketImpl = _basketImpl; ownerSplit = 0; //TODO: needed? }
Consider removing ownerSplit = 0;
, make the code simpler and save some gas.
#0 - 0xleastwood
2022-03-27T22:24:48Z
Duplicate of #45
🌟 Selected for report: ye0lde
Also found by: GiveMeTestEther, WatchPug, kenzo
WatchPug
function updateIBRatio(uint256 newRatio) onlyAuction external override returns (uint256) { ibRatio = newRatio; emit NewIBRatio(ibRatio); return ibRatio; }
The parameter of NewIBRatio
can use newRatio
directly to avoid unnecessary storage read of ibRatio
to save some gas.
Change to:
function updateIBRatio(uint256 newRatio) onlyAuction external override returns (uint256) { ibRatio = newRatio; emit NewIBRatio(newRatio); return newRatio; }
#0 - 0xleastwood
2022-03-27T22:29:08Z
Duplicate of #48
#1 - 0xleastwood
2022-03-27T22:44:06Z
Duplicate of #161
🌟 Selected for report: GiveMeTestEther
Also found by: WatchPug
18.4266 USDC - $18.43
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:
ibRatio
is used in for loop, cache and read from the stack can save gas.
function pullUnderlying(uint256 amount, address from) private { for (uint256 i = 0; i < weights.length; i++) { uint256 tokenAmount = amount * weights[i] * ibRatio / BASE / BASE; require(tokenAmount > 0); IERC20(tokens[i]).safeTransferFrom(from, address(this), tokenAmount); } }
Change to:
function pullUnderlying(uint256 amount, address from) private { uint256 _ibRatio = ibRatio; for (uint256 i = 0; i < weights.length; i++) { uint256 tokenAmount = amount * weights[i] * _ibRatio / BASE / BASE; require(tokenAmount > 0); IERC20(tokens[i]).safeTransferFrom(from, address(this), tokenAmount); } }
#0 - 0xleastwood
2022-03-27T22:17:05Z
Duplicate of #49
🌟 Selected for report: WatchPug
Also found by: GiveMeTestEther, TomFrenchBlockchain, danb, kenzo
WatchPug
function validateWeights(address[] memory _tokens, uint256[] memory _weights) public override pure { require(_tokens.length > 0); require(_tokens.length == _weights.length); uint256 length = _tokens.length; address[] memory tokenList = new address[](length); // check uniqueness of tokens and not token(0) for (uint i = 0; i < length; i++) { require(_tokens[i] != address(0)); require(_weights[i] > 0); for (uint256 x = 0; x < tokenList.length; x++) { require(_tokens[i] != tokenList[x]); } tokenList[i] = _tokens[i]; } }
for (uint256 x = 0; x < tokenList.length; x++)
can be change to for (uint256 x = 0; x < i; x++)
because the value of tokenList[i]
has not been set yet.
🌟 Selected for report: TomFrenchBlockchain
Also found by: WatchPug
18.4266 USDC - $18.43
WatchPug
function changePublisher(address newPublisher) onlyPublisher public override { require(newPublisher != address(0)); if (pendingPublisher.publisher != address(0) && pendingPublisher.publisher == newPublisher) { require(block.timestamp >= pendingPublisher.timestamp + TIMELOCK_DURATION); publisher = newPublisher; pendingPublisher.publisher = address(0); emit ChangedPublisher(publisher); } else { // ... } }
pendingPublisher.publisher
will never be address(0)
if newPublisher != address(0)
and pendingPublisher.publisher == newPublisher
.
Removing pendingPublisher.publisher != address(0)
can make the code simpler and save some gas.
Remove the redundant assertion.
#0 - 0xleastwood
2022-03-27T22:28:22Z
Duplicate of #41
🌟 Selected for report: WatchPug
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 approveUnderlying(address spender) private { for (uint256 i = 0; i < weights.length; i++) { IERC20(tokens[i]).safeApprove(spender, 0); IERC20(tokens[i]).safeApprove(spender, type(uint256).max); } }
tokens[i]
can be cached.
Change to:
function approveUnderlying(address spender) private { for (uint256 i = 0; i < weights.length; i++) { IERC20 token = IERC20(tokens[i]); token.safeApprove(spender, 0); token.safeApprove(spender, type(uint256).max); } }
🌟 Selected for report: WatchPug
WatchPug
The current implementation of auctionOngoing
is switching between true and false.
SSTORE
from false (0) to true (1) (or any non-zero value), the cost is 20000;
SSTORE
from 1 to 2 (or any other non-zero value), the cost is 5000.
By storing the original value once again, a refund is triggered (https://eips.ethereum.org/EIPS/eip-2200).
Since refunds are capped to a percentage of the total transaction's gas, it is best to keep them low, to increase the likelihood of the full refund coming into effect.
Therefore, switching between 1, 2 instead of false (0), true (1) will be more gas efficient.
Change to:
uint256 private constant _NOT_ONGOING = 1; uint256 private constant _ONGOING = 2; uint256 public override auctionOngoing;
function startAuction() onlyBasket public override { require(auctionOngoing == false, 'ongoing auction'); auctionOngoing = _ONGOING; // ... }
function killAuction() onlyBasket public override { auctionOngoing = _NOT_ONGOING; }
function initialize(address basket_, address factory_) public override { require(address(factory) == address(0)); require(!initialized); basket = IBasket(basket_); factory = IFactory(factory_); auctionOngoing = _NOT_ONGOING; initialized = true; }
function bondForRebalance() public override { require(auctionOngoing == _ONGOING); // ... }
function settleAuction( uint256[] memory bountyIDs, address[] memory inputTokens, uint256[] memory inputWeights, address[] memory outputTokens, uint256[] memory outputWeights ) public nonReentrant override { require(auctionOngoing == _ONGOING); require(hasBonded); require(bondTimestamp + ONE_DAY > block.timestamp); require(msg.sender == auctionBonder); require(inputTokens.length == inputWeights.length); require(outputTokens.length == outputWeights.length); for (uint256 i = 0; i < inputTokens.length; i++) { IERC20(inputTokens[i]).safeTransferFrom(msg.sender, address(basket), inputWeights[i]); } for (uint256 i = 0; i < outputTokens.length; i++) { IERC20(outputTokens[i]).safeTransferFrom(address(basket), msg.sender, outputWeights[i]); } //TODO: name a and b or further split up uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondBlock - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; (address[] memory pendingTokens, uint256[] memory pendingWeights, uint256 minIbRatio) = basket.getPendingWeights(); require(newRatio >= minIbRatio); IERC20 basketAsERC20 = IERC20(address(basket)); for (uint256 i = 0; i < pendingWeights.length; i++) { uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded); } basket.setNewWeights(); basket.updateIBRatio(newRatio); auctionOngoing = _NOT_ONGOING; hasBonded = false; basketAsERC20.safeTransfer(msg.sender, bondAmount); withdrawBounty(bountyIDs); emit AuctionSettled(msg.sender); }
function bondBurn() external override { require(auctionOngoing == _ONGOING); require(hasBonded); require(bondTimestamp + ONE_DAY <= block.timestamp); basket.auctionBurn(bondAmount); hasBonded = false; auctionOngoing = _NOT_ONGOING; basket.deleteNewIndex(); emit BondBurned(msg.sender, auctionBonder, bondAmount); auctionBonder = address(0); }
🌟 Selected for report: WatchPug
WatchPug
Functions can be defined outside of a contract, also called “free functions”, always have implicit internal visibility.
Every call to an external contract costs a decent amount of gas. For optimization of gas usage, use free functions to replace external calls can save gas.
For example:
Basket.sol#validateWeights()
can be defined as a free function, so that Factory.sol#proposeBasketLicense()
can save an external call.
basketImpl.validateWeights(tokens, weights);
🌟 Selected for report: WatchPug
WatchPug
There is no risk of overflow caused by increamenting the iteration index in for loops (the i++
in for for (uint256 i = 0; i < weights.length; i++)
).
Increments perform overflow checks that are not necessary in this case.
Surround the increment expressions with an unchecked { ... }
block to avoid the default overflow checks. For example, change the for loop:
for (uint256 i = 0; i < weights.length; i++) { uint256 tokenAmount = amount * weights[i] * ibRatio / BASE / BASE; require(tokenAmount > 0); IERC20(tokens[i]).safeTransferFrom(from, address(this), tokenAmount); }
to
for (uint256 i = 0; i < weights.length;) { uint256 tokenAmount = amount * weights[i] * ibRatio / BASE / BASE; require(tokenAmount > 0); IERC20(tokens[i]).safeTransferFrom(from, address(this), tokenAmount); unchecked { ++i; } }
🌟 Selected for report: WatchPug
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:
_bounties.push(Bounty({ token: address(token), amount: amount, active: true })); uint256 id = _bounties.length - 1;
_bounties.length - 1
will never underflow.
🌟 Selected for report: WatchPug
WatchPug
function initialize(address basket_, address factory_) public override { require(address(factory) == address(0)); require(!initialized); basket = IBasket(basket_); factory = IFactory(factory_); initialized = true; }
Auction.sol#initialize()
is using the factory_ parameter as the value of factory
, while Basket.sol#initialize()
uses msg.sender
.
function initialize(IFactory.Proposal memory proposal, IAuction auction_) external override { require(address(factory) == address(0)); require(!initialized); publisher = proposal.proposer; licenseFee = proposal.licenseFee; factory = IFactory(msg.sender); auction = auction_; ibRatio = BASE; tokens = proposal.tokens; weights = proposal.weights; maxSupply = proposal.maxSupply; approveUnderlying(address(auction)); __ERC20_init(proposal.tokenName, proposal.tokenSymbol); initialized = true; }
Consider changing to msg.sender
and removing the factory_
parameter for the purpose of consistency and gas saving.
🌟 Selected for report: WatchPug
WatchPug
function initialize(IFactory.Proposal memory proposal, IAuction auction_) external override { require(address(factory) == address(0)); require(!initialized); publisher = proposal.proposer; licenseFee = proposal.licenseFee; factory = IFactory(msg.sender); auction = auction_; ibRatio = BASE; tokens = proposal.tokens; weights = proposal.weights; maxSupply = proposal.maxSupply; approveUnderlying(address(auction)); __ERC20_init(proposal.tokenName, proposal.tokenSymbol); initialized = true; }
factory
will always be address(0)
when initialized == false
as the only way to set factory
is initialize()
.
Removing require(address(factory) == address(0));
can make the code simpler and save some gas.
Remove the redundant assertion at L45.
WatchPug
The following source units are imported but not referenced in the contract:
import "hardhat/console.sol";
import "hardhat/console.sol";
import "hardhat/console.sol";
Check all imports and remove all unused/unreferenced and unnecessary imports.