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: 5/37
Findings: 3
Award: $1,825.17
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: gellej
Also found by: Czar102, TomFrenchBlockchain, WatchPug, csanuragjain, defsec, hubble, p4st13r4, pedroais
BadgerDAO is both incentivised and capable of arbitrarily upgrading the logic of the smart contract to retain both CTDL and user's funds.
The process of buying CTDL from TokenSaleUpgradeable
goes as follows:
buy
finalize
function.claim
function to withdraw CTDL tokens.Both the CTDL and WBTC tokens will then sit inside the TokenSaleUpgradeable
contract until BadgerDAO calls the finalize
function.
BadgerDAO is in a unique position with regards to this contract as they are simultaneously one of the parties to the sale as well as the admin over the TransparentUpgradeableProxy
through which TokenSaleUpgradeable
is made available. They may then upgrade the logic of the contract arbitrarily, allowing them to withdraw both the WBTC and CTDL instead of finalising the sale.
BadgerDAO makes usage of proxy contracts extensively so this may not be immediately considered an issue, however in this example BadgerDAO's incentives are directly opposed to that of the smart contract's users as opposed to them being in alignment as for it's vault contracts.
We can consider this contract to be equivalent to an escrow contract which includes a function allowing one of the two parties to unilaterally withdraw both parties' funds. I think it's hard to argue that such an escrow contract would be a suitable implementation and so it should follow for this contract too.
The contest readme also explicitly asks for rug vectors of which this is a clear example.
Ideally implement the contract without the use of a proxy. As the sale is intended as a one off there's little need for upgradability and this will allow many gas savings as well as avoiding rug potential.
If it is still desired to use a proxy pattern then TokenSaleUpgradeable
should be modified to atomically send out CTDL to buyers on the buy
call to avoid holding both a buyer's funds and their bought CTDL simultaneously.
#0 - 0xleastwood
2022-03-14T11:39:57Z
Duplicate of #50
https://github.com/code-423n4/2022-02-badger-citadel/blob/84596551d62f243d13fcb2d486346dde08002f7b/contracts/TokenSaleUpgradeable.sol#L144-L148 https://github.com/code-423n4/2022-02-badger-citadel/blob/84596551d62f243d13fcb2d486346dde08002f7b/contracts/TokenSaleUpgradeable.sol#L216-L224 https://github.com/code-423n4/2022-02-badger-citadel/blob/84596551d62f243d13fcb2d486346dde08002f7b/contracts/TokenSaleUpgradeable.sol#L300-L309
Buyer's cannot set a minimum amount of CTDL that they will expect so BadgerDAO can force them to receive a smaller amount than expected.
The buy
function only allows the buyer to specify an amount of tokenIn
to send to the contract and will accept any amount of tokenOut
that they receive in return. Simultaneously, BadgerDAO may set tokenOutPrice
at any point in the sale to arbitrary values.
buy
transaction sending 10 WBTC expecting to receive 10 CTDL in returnsetTokenOutPrice
to set tokenOutPrice = max_uint256
As both the seller and the admin of the contract, BadgerDAO is both incentivised and able to manipulate tokenOutPrice
throughout the sale in order to maximise the amount of WBTC they receive.
See similar issue: https://github.com/code-423n4/2021-05-nftx-findings/issues/51
#0 - GalloDaSballo
2022-02-10T02:13:54Z
Ultimately agree with the finding, I think making the price unchangeable or adding a slippage check is the way to go
#1 - 0xleastwood
2022-03-14T11:41:24Z
Duplicate of #50 but warden already has an issue included for awards consideration.
#2 - 0xleastwood
2022-03-16T13:07:15Z
Duplicate of #105
130.1858 USDC - $130.19
TokenSaleUpgradeable
is expected to be used to perform a one-off sale for a short period of time after which it will no longer be used. It's then very unlikely that the implementation should legitimately need to be upgraded and so a proxy pattern is unnecessary.
If this was not deployed through a proxy we'd save both the costs of interaction with the proxy contract as well as allow the use of gas-saving measures such as immutable variables. Considering the number of storage reads performed as well as the fact that many of these values likely do not need to be updated after deployment this would be a significant gas saving.
getAmountOut
makes a tokenOut.decimals()
call each time it is called. As the decimals value for the CTDL token will not change over time the value 10**tokenOut.decimals
can be cached in this contract to avoid the costs of an external call.
Alternatively, this value could be rolled into tokenOutPrice
to avoid having to perform either the call or an SLOAD.
Here we read saleStart
from storage rather than caching in memory resulting in an extra SLOAD
Many of the variables use a full uint256
slot whereas their maximum value will almost certainly fit in a much smaller value. The contract owner can control the maximum possible value for the linked variables and so can safely use smaller types so that they may be packed into storage.
e.g. if tokenInLimit
is set to fit in a uint128
then it can clearly be packed with totalTokenIn
.
https://github.com/code-423n4/2022-02-badger-citadel/blob/84596551d62f243d13fcb2d486346dde08002f7b/contracts/TokenSaleUpgradeable.sol#L31-L33 https://github.com/code-423n4/2022-02-badger-citadel/blob/84596551d62f243d13fcb2d486346dde08002f7b/contracts/TokenSaleUpgradeable.sol#L42-L50