Platform: Code4rena
Start Date: 17/03/2022
Pot Size: $30,000 USDC
Total HM: 8
Participants: 43
Period: 3 days
Judge: gzeon
Total Solo HM: 5
Id: 100
League: ETH
Rank: 9/43
Findings: 4
Award: $1,187.27
π Selected for report: 0
π Solo Findings: 0
π Selected for report: GreyArt
Also found by: 0xDjango, CertoraInc, TomFrenchBlockchain, WatchPug, cmichel, rayn
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/Collateral.sol#L52 https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/SingleStrategyController.sol#L32
This bug enables partial bypass of fee while minting Collateral tokens through Collateral.deposit
.
Attackers can also utilize this bug to inflate prices of Collateral tokens, creating "unfair advantages" for early minters of Collateral tokens.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
In Collateral.deposit
, the amount to deposit is calculated several times.
_amount
is used to fetch how much to transfer from msg.sender
to Collateral
_baseToken.safeTransferFrom(msg.sender, address(this), _amount);
_baseToken
of Collateral
is fetched for fee calculationuint256 _amountToDeposit = _baseToken.balanceOf(address(this)); ... uint256 _fee = (_amountToDeposit * _mintingFee) / FEE_DENOMINATOR + 1; require(_amountToDeposit > _fee, "Deposit amount too small"); _baseToken.safeTransfer(_treasury, _fee); _amountToDeposit -= _fee;
_strategyController.totalValue()
is used to recalculate _amountToDeposit
. Apart from this, _strategyController
deposits _baseToken.balanceOf(address(this))
to _stategy
instead of using _amount
passed to itCollateral function deposit(uint256 _amount) external override nonReentrant returns (uint256) { ... uint256 _valueBefore = _strategyController.totalValue(); _baseToken.approve(address(_strategyController), _amountToDeposit); _strategyController.deposit(_amountToDeposit); uint256 _valueAfter = _strategyController.totalValue(); _amountToDeposit = _valueAfter - _valueBefore; } SingleStrategyController function deposit(uint256 _amount) external override onlyVault nonReentrant { _baseToken.safeTransferFrom(_vault, address(this), _amount); _strategy.deposit(_baseToken.balanceOf(address(this))); } function totalValue() external view override returns (uint256) { return _baseToken.balanceOf(address(this)) + _strategy.totalValue(); }
The problem is _baseToken.balanceOf(address(this))
in _strategyController.deposit
is susceptible to manipulation. An attacker can simply transfer some _baseToken
to _strategyController
directly before calling Collateral.deposit
to make the passed _amount
and _baseToken.balanceOf(address(this))
mismatch
However, while the attacker is able to deposit more _baseToken
to _strategy
than expected, the calculated _shares
does not increase since it relies on _amountToDeposit
calculated from _strategyController.totalValue()
, which is not affected by the manipulation of _baseToken
balance of _strategyController
uint256 _shares = 0; if (totalSupply() == 0) { _shares = _amountToDeposit; } else { /** * # of shares owed = amount deposited / cost per share, cost per * share = total supply / total value. */ _shares = (_amountToDeposit * totalSupply()) / (_valueBefore); } _mint(msg.sender, _shares);
The immediate consequence of increased _baseToken
deposited and unaffected _shares
is that less Collateral tokens are minted than expected, leading to the inflation of the Collateral tokens, and create an "unfair" advantage for minters before inflation to those that joined after inflations.
To sum up the attack, the attacker creates a contract to do those actions listed below in a single transaction
_baseToken.deposit(AMOUNT1+AMOUNT2+AMOUNT3)
Collateral.deposit(AMOUNT1)
_baseToken.transfer(_strategyController, AMOUNT2)
Collateral.deposit(AMOUNT3)
<- this triggers inflationThe effect of the attack is
_baseTokens
into _strategy
None
Since it is reasonable to disallow users from transferring _baseToken
to _strategyController
directly, an easy remedy is to change _strategyController
to deposit _amount
to _strategy
SingleStrategyController function deposit(uint256 _amount) external override onlyVault nonReentrant { _baseToken.safeTransferFrom(_vault, address(this), _amount); _strategy.deposit(_amount); }
To prevent the token lockup if anyone accidentally transfers to _strategyController
, it is possible to utilize that _strategyController
should never own any token except during Collateral.deposit
, and add a skim
function to _strategyController
to allow anyone to remove excessive tokens in the contract.
SingleStrategyController function skim(address to) external override onlyVault nonReentrant { _baseToken.safeTransferFrom(to, _baseToken.balanceOf(address(this))); }
#0 - ramenforbreakfast
2022-03-24T03:47:45Z
duplicate of #27
#1 - gzeoneth
2022-04-03T13:31:01Z
Agree with sponsor
π Selected for report: defsec
Also found by: 0x1f8b, 0xDjango, 0xNazgul, 0xkatana, 0xwags, CertoraInc, Funen, GeekyLumberjack, GreyArt, IllIllI, Kenshin, Ruhum, TerrierLover, WatchPug, berndartmueller, bugwriter001, cccz, cmichel, csanuragjain, hake, kenta, kirk-baird, leastwood, minhquanym, oyc_109, peritoflores, rayn, remora, rfa, robee, saian, samruna, sorrynotsorry, wuwe1
101.302 USDC - $101.30
In document, we define that:
### Expiry The expiry date of the market. If a market has not settled by its expiry date, it will automatically settle at the lower bound of its Valuation Range.
https://docs.prepo.io/concepts/markets#expiry
But in contract, the variable _expiryTime
doesn't settle at the lower bound of its Valuation Range.
Somebody can still mint LongShortTokens if market expired and owner doesn't set _floorLongPrice
.
Check _expiryTime
in mintLongShortTokens:
if (block.timestamp > _expiryTime && _finalLongPrice == MAX_PRICE + 1) { _finalLongPrice = _floorLongPrice; }
In DepositHook.sol and WithdrawHook.sol: https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/DepositHook.sol#L28 https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/WithdrawHook.sol#L23
The parameter _initialAmount
is unused.
Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. --> contracts/core/DepositHook.sol:28:9: | 28 | uint256 _initialAmount, | ^^^^^^^^^^^^^^^^^^^^^^ Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. --> contracts/core/WithdrawHook.sol:23:9: | 23 | uint256 _initialAmount, |
Delete _initialAmount
parameter.
In initialize()
of PrePOMarketFactory.sol
:
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/PrePOMarketFactory.sol#L20
The contract PrePOMarketFactory
inherit ReentrancyGuardUpgradeable
but not initialize it.
Change to:
20 function initialize() public initializer { 21 OwnableUpgradeable.__Ownable_init(); + 22 ReentrancyGuardUpgradeable.__ReentrancyGuard_init(); 23 }
In _createPairTokens
of PrePOMarketFactory.sol
:
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/PrePOMarketFactory.sol#L117
It's unnecessary to return value in L117 due to provides names for variables in L98: https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/PrePOMarketFactory.sol#L98
Delete L117.
allowSelf
doesn't revert when blocked account call this functionIn the definition of allowSelf
in IAccountAccessController.sol
:
/** * @notice Allows the caller if the provided signature is valid. * @dev An account cannot call this function if it is already * allowed/blocked. * @param proof Proof of the caller's inclusion in the merkle root */ function allowSelf(bytes32[] calldata proof) external;
An account cannot call this function if it is already allowed/blocked, but a blocked account can still call this function.
Check the account is blocked in allowSelf
function:
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/AccountAccessController.sol#L61
function allowSelf(bytes32[] calldata _proof) external override { require( _allowedAccounts[_allowedAccountsIndex][msg.sender] == false, "Account already registered" ); + require( + _blockedAccounts[_blockedAccountsIndex][msg.sender] == false, + "Account already blocked"
#0 - ramenforbreakfast
2022-03-24T03:40:36Z
Expiry issue - duplicate of #28 Unused parameter - duplicate of #4 Uninitialized parent contract - duplicate of #13 Redundant code for return value - valid, more of a gas optimization/0 severity issue allowSelf - misunderstanding of allow/blocked due to our documentation. duplicate of #35
28.6158 USDC - $28.62
The for loops in allowAccounts
and blockAccounts
:
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/AccountAccessController.sol#L44-L47
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/AccountAccessController.sol#L55-L58
There is no overflow risk of _i
.
Use unchecked
block to avoid overflow checks:
- for (uint256 _i = 0; _i < _accounts.length; _i++) { + for (uint256 _i = 0; _i < _accounts.length; ) { _allowedAccounts[_allowedAccountsIndex][_accounts[_i]] = true; emit AccountAllowed(_accounts[_i]); + unchecked { + ++_i; + }
Currently the Collateral contract approve _strategyController
to transfer the base token:
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/Collateral.sol#L76
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/SingleStrategyController.sol#L38
We can send base token to _strategyController
directly to save gas.
Change in Collateral.sol:
@@ -73,7 +73,10 @@ contract Collateral _amountToDeposit -= _fee; uint256 _valueBefore = _strategyController.totalValue(); - _baseToken.approve(address(_strategyController), _amountToDeposit); + _baseToken.safeTransfer( + address(_strategyController), + _amountToDeposit + ); _strategyController.deposit(_amountToDeposit); uint256 _valueAfter = _strategyController.totalValue(); _amountToDeposit = _valueAfter - _valueBefore;
Change in SingleStrategyController.sol:
@@ -35,7 +35,6 @@ contract SingleStrategyController is onlyVault nonReentrant { - _baseToken.safeTransferFrom(_vault, address(this), _amount); _strategy.deposit(_baseToken.balanceOf(address(this))); }
#0 - ramenforbreakfast
2022-03-24T03:34:22Z
Unchecked arithmetic - duplicate of #18 Send _baseToken in deposit - duplicate of #31