Platform: Code4rena
Start Date: 09/12/2022
Pot Size: $36,500 USDC
Total HM: 9
Participants: 69
Period: 3 days
Judge: Picodes
Total Solo HM: 2
Id: 190
League: ETH
Rank: 13/69
Findings: 3
Award: $583.62
🌟 Selected for report: 0
🚀 Solo Findings: 0
530.4488 USDC - $530.45
Mint/Redeem Hooks are not set in the constructor, but with a separate transaction. If the owner wants to add a mint/redeem hook that requires msg.sender validation, an adversary can back run the market-creation/front-run the hook-setting transaction to allow them to mint/redeem where there normally would be rejected due to some action or requirement being implemented in the mint-hook
Include IMarketHook parameters for _mintHook and _redeemHook in the constructor so that all mints are required to obey hook rules from the start. The address can be zero if the owner chooses not to include a hook but should have the option to.
#0 - c4-judge
2022-12-19T10:39:16Z
Picodes marked the issue as duplicate of #312
#1 - c4-judge
2023-01-07T11:35:56Z
Picodes marked the issue as satisfactory
🌟 Selected for report: 0xSmartContract
Also found by: 0Kage, 0x52, 0xAgro, 0xNazgul, 0xTraub, 0xhacksmithh, Awesome, Aymen0909, Bnke0x0, Englave, Janio, Mukund, Parth, RaymondFam, Rolezn, SmartSek, Tointer, UdarTeam, Udsen, Zarf, caventa, chaduke, csanuragjain, deliriusz, gz627, idkwhatimdoing, izhelyazkov, joestakey, neumo, obront, oyc_109, rvierdiiev, shark, trustindistrust, wait, yongskiws
28.124 USDC - $28.12
--- PrePOMarket.sol ---
Does not use OpenZeppelin SafeERC20 for IERC20
collateral.transferFrom(msg.sender, address(this), _amount);
in mint
does not include validation that the transferFrom succeeded.
If the function does not revert on failure but returns "false" then the user may still mint tokens without supplying collateral
Solution: Use safeTransferLib or require(success) on transferFrom
Should use SafeApprove for collateral approvals in the event where non-standard approval requires first zero-ing out approvals to increase or to use increaseAllowance
Decimal Mismatch from OpenZeppelin ERC20. The OpenZeppelin ERC20 token file enforces 18 decimals of precision unless explicitly overwritten. This means all long-short tokens will be default 18-decimals precision. In the mint/redeem functions the amount of tokens minted/burned is based on some amount of collateral, I.E if USDC/USDT, which has 6-decimals of precision, is used as collateral to mint, then 1e6 amount of long-short tokens will be minted to the user. This means that there is now a mismatch between number of decimals stated in the ERC20 decimals()
method, and the actual number of decimals used for tracking balances of tokens. Given that external protocols may do calculations based on the stated number of decimals to save precision, this can lead to misc. rounding-errors. It may also complicated/break front ends which rely on proper decimal numbers to display properly formatted balances to users.
https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/PrePOMarket.sol#L70-L71 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L87-L89
Solution: Modify LongShortToken.sol to set the number of decimals to the same as the collateral number, or do dynamic scaling at the time of mint/redeem to scale from collateral-decimals up/down to 18 when non-18-decimal collateral is used.
--- Collateral.sol ---
The redeem()
function includes a check that if (address(_redeemHook) != address(0))
to call _redeemHook.hook
. However, setRedeemHook
does not check that the new redeemHook address actually implements that function. As a result the ownership can set a non-zero address that does not implement the function. The external call will fail and any redemptions will as well since the hook needs to be successfully called before redemptions can be processed.
Solution: Implement a supportsInterface(IMarketHook)
check on any new redeemHook addresses being set by ownership
setManager(address _newManager)
does not have a require(_newManager != address(0))
check--- DepositTradeHelper.sol ---
IERC20Permit(address(_baseToken)).permit(msg.sender, address(this), type(uint256).max, baseTokenPermit.deadline, baseTokenPermit.v, baseTokenPermit.r, baseTokenPermit.s);
For amount to permit consider using baseAmount
instead of type(uint256).max, which causes an approval for unlimited tokens. First, depending on the token, this may cause problems if the amount to approve must first be zero since you are not first zero-ing out the allowance. Second, since the user is always signing a permit message it is better for security to only permit the number of tokens needed to complete the transaction, resulting in a more secure allowance of zero outside of the transaction, otherwise they will always be approving the contract to spend max-tokens which costs gas and is unnecesarry.
if (baseTokenPermit.deadline != 0)
, consider using if deadline > block.timestamp
to ensure that the deadline is in the future, and preventing any signatures from being rejected by the ERC20 permit function. It also serves the same function since a permit deadline of zero would result in bypassing that statement anyways.--- PrePOMarketFactory.sol ---
initialize() function can be called by anybody when the contract is not initialized.
More importantly, if someone else runs this function, they will have full authority because of the __Ownable_init() function. Also, there is no 0 address check in the address arguments of the initialize() function, which must be defined.
Solution: Specify that only a pre-defined initializer address is allowed to call the function `require (msg.sender == INITIALIZER_ADDR);
--- Misc ---
All events triggered on ownership changing a state-variable should include the original value and the new value. Examples include
#0 - c4-judge
2022-12-19T14:37:04Z
Picodes marked the issue as grade-b
#1 - ramenforbreakfast
2022-12-21T22:24:31Z
I think this report should be disregarded, when compared to other reports.
Decimal mismatch issue above doesn't make any sense because the underlying baseToken decimals do not affect the fact that Collateral
use to mint Long and Short tokens will be always 18 decimals.
Suggestion to have supportInterface
on RedeemHook
seems to be overly complex and not actually provide any concrete protection.
The suggestion around permits ignores the fact that the FE wouldn't be submitting valid permits and just submit junk data if a user already has approved, so there wouldn't be time wasted (this is documented).
Overall, the suggestions and format of this report are not up to par with others.
#2 - c4-sponsor
2022-12-21T23:23:17Z
ramenforbreakfast marked the issue as sponsor disputed
#3 - Picodes
2023-01-07T18:23:24Z
Keeping grade b as I still think this submission does not deserve a c although it's true that the formatting could be improved significantly.
🌟 Selected for report: ReyAdmirado
Also found by: 0xSmartContract, 0xTraub, Aymen0909, Englave, Mukund, RHaO-sec, RaymondFam, Rolezn, Sathish9098, Tomio, UdarTeam, chaduke, dharma09, gz627, martin, nadin, pavankv, rjs, saneryee
25.0472 USDC - $25.05
PrePOMarket.sol
require(longToken.balanceOf(msg.sender) >= _longAmount, "Insufficient long tokens"); require(shortToken.balanceOf(msg.sender) >= _shortAmount, "Insufficient short tokens");
These can both be removed since later on you call
longToken.burnFrom(msg.sender, _longAmount); shortToken.burnFrom(msg.sender, _shortAmount);
Since you can never burn more tokens than available to the user, the check is redundant and burnFrom will fail if _longAmount is > balanceOf(msg.sender) anyways. Saves gas from the two checks not needing to occur
require(collateral.balanceOf(msg.sender) >= _amount, "Insufficient collateral");
Is redundant. On line 69 collateral.transferFrom(msg.sender, address(this), _amount);
, as long as transferFrom success is properly validated and reverts on failure, you do not need to check that the their balance >= _amount, since transferFrom will fail anyways due to insufficient balance. Saves gas on the external balanceOf call and the comparison not needing to occur
--- DepositRecord.sol ---
You already checked for maximum values in the require statements above so you can wrap these last two lines in an unchecked
block since there's no chance of overflow since userDepositCap <= type(uint).max.
Instead of using if-else which costs additional gas through jumps, replace with a require(_amount <= globalNetDepositAmount)
and then in an unchecked block unchecked {globalNetDepositAmount -= amount}
since there's no chance of an underflow because amount cannot be greater than globalNetDepositAmount
#0 - c4-judge
2022-12-19T13:24:13Z
Picodes marked the issue as grade-b