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: 4/37
Findings: 4
Award: $1,938.19
🌟 Selected for report: 0
🚀 Solo Findings: 0
I thought of a potential rug pull from the owner: when users buy tokenOut, it is not required that the contract has already escrowed enough tokenOut. It is only required when finalizing the sale:
require( tokenOut.balanceOf(address(this)) >= totalTokenOutBought, "TokenSale: not enough balance" );
This doesn't look fair from the perspective of users. Users have to trust that after the sale the owner will send enough tokens and finalize the sale. This increases the trust of one entity, while the goal of smart contracts should be to minimize this. When designing a smart contract, it should aim for as fair model as possible. Now the owner of the contract is privileged, the saleRecipient instantly receives tokenIn, but users will have to wait till the owner finalizes the sale.
I thought if this should be submitted as of low severity, but because this is explicitly mentioned in the README: "Specific care should be put in: Rug Vectors" I decided to submit this as of medium severity.
Suggestions:
#0 - GalloDaSballo
2022-02-10T01:49:52Z
I agree with the finding, instead of transferring to the multisig we should use the sale contract as escrow, we should also add a ragequit
function that allows depositors to get their initial deposit back if the owner doesn't finalize within X time
#1 - 0xleastwood
2022-03-14T12:07:21Z
Duplicate of #50
#2 - 0xleastwood
2022-03-16T12:27:14Z
Duplicate of #61
https://github.com/code-423n4/2022-02-badger-citadel/blob/main/contracts/TokenSaleUpgradeable.sol#L303-L309 https://github.com/code-423n4/2022-02-badger-citadel/blob/main/contracts/TokenSaleUpgradeable.sol#L223
Users should be able to control the accepted price. The owner can anytime invoke function setTokenOutPrice and thus change the ratio of token in/out. Users have to trust the owner not to front-run them and make the tokens more expensive.
It would be fair if users can specify the (min) price, or (min) token out the amount that they are happy with when calling the buy function, so in case it is updated before the tx is mined, it will not execute the buy.
#0 - GalloDaSballo
2022-02-10T01:48:19Z
I think the issue of owner privilege has been mentioned by other wardens, and this may end up being used as a duplicate.
I think this solution of adding a slippage check on buy
is welcomed and we should probably add it (or make the price immutable)
#1 - 0xleastwood
2022-03-14T12:08:10Z
Duplicate of #50 but warden already has an issue that is being considered under this.
#2 - 0xleastwood
2022-03-16T13:10:28Z
Duplicate of #105
87.9099 USDC - $87.91
TODO: Better revert strings
Consider introducing a claim deadline. after the deadline, unclaimed tokens can be swept or burned by the owner. This would prevent tokens from being stuck forever in the contract in case a user is unavailable to claim them.
Consider introducing reasonable upper limits for saleStart and saleDuration. This would further reduce the trust in the owner, who can grief by e.g. calling setSaleStart very far in the future, or setSaleDuration too long for our generation to finish, thus making users never receive their tokens. Think of reasonable values, for instance, saleStart max 1 month in the future, and when updating validate it against the initial value, and saleDuration max 1 year, or something like that. Or these values could be configurable on initialization but should be transparent (users can validate).
The comment says that it pauses the sale:
* @notice Pause the sale. Can only be called by owner function pause() external onlyOwner
But whenNotPaused modifier is also applied to the function claim which is invokable after the sale has been finalized. This again increases the trust of the owner, who can pause the claims leaving users no choice to claim their tokens. However, I also understand that from your point of view, this would leave no option to stop claims in case of an emergency situation. So submitting this FYI to decide if changes are necessary here.
function setTokenOutPrice should also validate !finalized. While this does not cause any harm, it makes it more coherent if the price can't be updated once the sale is finalized.
function buy can backrun initialize when saleStart = block.timestamp:
require(saleStart <= block.timestamp, "TokenSale: not started");
In my opinion, it would be better to require _saleStart > block.timestamp in initialize function so that all the buys can't come in the same block. But I do not see any direct harm of this, so it is just a suggestion.
#0 - GalloDaSballo
2022-02-10T01:46:00Z
Appreciate the findings
45.891 USDC - $45.89
10**tokenOut.decimals()
if (totalTokenIn < tokenInLimit) { limitLeft_ = tokenInLimit - totalTokenIn; }
require( totalTokenIn + _tokenInAmount <= tokenInLimit, "total amount exceeded" ); ... totalTokenIn += _tokenInAmount;
with:
totalTokenIn += _tokenInAmount; require( totalTokenIn <= tokenInLimit, "total amount exceeded" ); ...
require(saleStart <= block.timestamp, "TokenSale: not started"); require( block.timestamp < saleStart + saleDuration, "TokenSale: already ended" );