Platform: Code4rena
Start Date: 04/02/2022
Pot Size: $30,000 USDC
Total HM: 3
Participants: 37
Period: 3 days
Judge: leastwood
Id: 84
League: ETH
Rank: 8/37
Findings: 4
Award: $1,287.74
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: gellej
Also found by: Czar102, TomFrenchBlockchain, WatchPug, csanuragjain, defsec, hubble, p4st13r4, pedroais
The current implementation requires trusted key holders (Owner
) to send transactions (finalize()
) to finalize the sale before the buyers can claim()
the tokenOut
from the contract.
function finalize() external onlyOwner { require(!finalized, "TokenSale: already finalized"); require(saleEnded(), "TokenSale: not finished"); require( tokenOut.balanceOf(address(this)) >= totalTokenOutBought, "TokenSale: not enough balance" ); finalized = true; emit Finalized(); }
This introduces a severe centralization risk, which can cause funds to be frozen in the contract if the key holders lose access to their keys.
Given:
totalTokenOutBought
= 4M CTDLIf the Owner
lose access to their keys ("hit by a bus"). Then the 4M CTDL
will be frozen in the contract as no one can finalize()
, furthermore, all the buyers wont be able to claim()
the CTDL
tokens they bought.
Consider allowing finalize()
to be called by anyone as along as the sale is ended and there is enough tokenOut
balance in the contract.
#0 - 0xleastwood
2022-03-14T10:29:19Z
Duplicate of #20 but marking this as the primary issue.
#1 - 0xleastwood
2022-03-14T10:48:26Z
Duplicate of #50
In TokenSaleUpgradeable.sol#buy()
, tokenIn
will be transferred from the buyer directly to the saleRecipient
without requiring/locking/releasing the correspoining amount of tokenOut
.
This allows the saleRecipient
to rug the users simply by not transferring tokenOut
and finalizing the sale.
Given:
WBTC
1e8
CTDL
buy()
with 100e8
;buy()
with 200e8
;A malicious saleRecipient
can just not transfer any CTDL
to the contract and finalize()
and keep 300e8 WBTC
received.
As a result, Alice and Bob can not get the expected amount of tokensOut
, and there is no way to retrieve the WBTC paid, in essence, lose all the funds.
Instead of transferring the tokenIn directing to the saleRecipient
in buy()
, consider transferring the tokenIn into the contract (address(this)
), and require a sufficient amount of tokenOut
to be transferred into the contract first before the amount of tokenIn
can be released to the saleRecipient
.
#0 - 0xleastwood
2022-03-14T10:32:54Z
As this is also an issue regarding abuse of an owner's admin privileges, it fits the criteria of a medium
severity issue.
#1 - 0xleastwood
2022-03-16T12:21:12Z
I've thought about this more and I've decided to split up distinct issues into 3 primary issues:
saleRecipient
before settlement.#2 - 0xleastwood
2022-03-16T12:21:25Z
This issue falls under the second primary issue.
saleRecipient
before settlement.69.7508 USDC - $69.75
We find the 2022-02-badger-citadel
codebase well-documented, well-structured, with a fair amount of tests, and pretty gas efficient.
There are 2 Non-critical issues and 1 Low severity issue found.
Some of the error messages are prefixed with TokenSale:
while some are not.
Consider updating the error messages to keep the style of error messages consistent.
There are ERC20 tokens that charge fee for every transfer()
or transferFrom()
.
In the current implementation, TokenSaleUpgradeable.sol#buy()
assumes that the received amount is the same as the transfer amount, and uses it to calculate tokenOutAmount_
.
Consider calling balanceOf()
before and after the transfer to get the actual transferred amount if a token with transfer tax as tokenIn
should be supported.
setTokenOutPrice
after the token sale starts can result in unexpected results for buyersIn the current implementation, the owner can call setTokenOutPrice()
and change the tokenOutPrice anytime, including when the token sale already started. In the case of network congestion or in chance, if the owner setTokenOutPrice()
to a higher price, it can result in unexpected tokenOutAmount for the buyers who submitted their buy()
txs before but only get packed into the block after the setTokenOutPrice()
tx.
We consider this an undesirable situation for users and there is pretty much no other way to prevent it, therefore it should be prevented at the smart contract level.
Consider making setTokenOutPrice()
only callable before the sale starts.
#0 - GalloDaSballo
2022-02-14T13:03:52Z
I think the report is well thought out, I believe we will use wBTC
as the inToken so there won't be an issues with fees, but we may have missed that in the ReadME.
All in all I think the report is valid
77.0216 USDC - $77.02
Every call to an upgradable contract must be redirected by the proxy contract with an external call. Therefore, there is a gas overhead for every call to the contract.
Plus, using an upgradeable proxy make it hard to utilize some other gas optimization such as immutable variables in the constructor function.
We recommend removing the proxy and making the contract non-upgradeable.
Considering that this can save a significant amount of gas, we mark this as a Suggested
level gas optimization point.
See also: [WP-H1]
Considering that tokenOut
, tokenIn
will never change, changing them to immutable variables instead of storages variable can save gas.
This optimazation depends on S1
.
function getAmountOut(uint256 _tokenInAmount) public view returns (uint256 tokenOutAmount_) { tokenOutAmount_ = (_tokenInAmount * 10**tokenOut.decimals()) / tokenOutPrice; }
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.
tokenOut.decimals()
can be cached as a storage/immutable variable, since tokenOut
can not be changed, and the decimals
of tokenOut
are unlikely to be changed.
saleEnd
to replace saleStart + saleDuration
can save gasblock.timestamp < saleStart + saleDuration
-> block.timestamp < saleEnd
The storage variable saleDuration
is commonly used with another saleStart
together to compare with block.timestamp
, this includes 2 SLOAD
and 1 ADD
.
We recommend replacing saleDuration
with saleEnd
, so that it can be reduced to 1 SLOAD
only.
!= 0
can be more gas efficient than > 0
in require
statementsrequire(amount > 0, "...");
-> require(amount != 0, "...");
Considering that the difference in gas is insignificant and it cloud makes it a bit less readable, we mark this as Non-preferred
.
#0 - GalloDaSballo
2022-02-14T13:04:58Z
The findings are all valid. Technically point 1 and 2 require us to change the codebase and loose functionality so I'd say that's a nofix for us