Open code423n4 opened 1 year ago
Making Primary for most detail on impact
GalloDaSballo marked the issue as primary issue
TriHaz marked the issue as sponsor confirmed
In contrast to other CEI reports, this report shows how control can be gained in the middle of the mint execution to create an inconsistent state
The warden has shown how, because mint
doesn't follow CEI conventions, by reEntering via safeMint, an attacker can manipulate the state of limit orders, and also benefit by changing profit calculations.
Because the finding shows how to break invariants and profit from it, I agree with High Severity
GalloDaSballo marked the issue as selected for report
Lines of code
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/Position.sol#L126-L161
Vulnerability details
Impact
Function
Position.mint()
has been used ininitiateLimitOrder()
andinitiateMarketOrder()
and it doesn't follow check-effect-interaction pattern and code updates the values of_limitOrders
,initId
,_openPositions
andposition _tokenIds
variables after making external call by usingsafeMint()
. This would give attacker opportunity to reenter the Trading contract logics and perform malicious action while contract storage state is wrong. the only limitation of the attacker is that he need to bypass_checkDelay()
checks. attacker can perform this action:initiateLimitOrder()
and create limit order with id equal to ID1 reenter (while_limitOrders
for ID1 is not yet settled) withcancelLimitOrder(ID1)
(nocheckDelay()
check) and remove other users limit orders because code would try to remove_limitOrderIndexes[_asset][ID1]
position but the value is 0 and code would remove limit order in the index 0 which belongs to another user in thePosition.burn()
code.initiateMarketOrder()
and create a position with ID1 and whileinitId[ID1]
has not yet settled reenter the Trading withaddToPosition(ID1)
function (bypasscheckDelay()
because both action is opening) and increase the position size which would setinitId[ID1]
according to new position values but then when code execution returns to rest ofmint()
logicinitId[ID1]
would set by initial values of the positions which is very lower than what it should be andinitId[ID1]
has been used for calculatingaccuredInterest
of the position which is calculated for profit and loss of position and contract would calculate more profit for position and would pay attacker more profit from contract balances.Proof of Concept
This is
mint()
code in Position contract:As you can see by calling
_safeMint()
code would make external call toonERC721Received()
function of the account address and the code sets the values for_limitOrders[]
,_limitOrderIndexes[]
,initId[]
,_openPositions[]
,_openPositionsIndexes[]
,_assetOpenPositions[]
,_assetOpenPositionsIndexes[]
and_tokenIds
. so code don't follow check-effect-interaction pattern and it's possible to perform reentrancy attack. there could be multiple scenarios that attacker can perform the attack and do some damage. two of them are:scenario #1 where attacker remove other users limit orders and create broken storage state
initiateLimitOrder()
and code would create the limit order and mint it in thePosition._safeMint()
with ID1._safeMint()
function because of theonERC721Received()
call check._limitOrders[]
,_limitOrderIndexes[ID1]
are not yet updated for ID1 and_limitOrderIndexes[ID1]
is 0x0 and ID1 is not in_limitOrder[]
list.cancelLimitOrder(ID1)
.cancelLimitOrder()
checks would pass and would tries to callPosition.burn(ID1)
.burn()
function would tries to remove ID1 from_limitOrders[]
list but because_limitOrderIndexes[ID1]
is 0 so code would remove the 0 index limit order which is belongs to another user.Position.mint()
logic and code would add burned id token to_limitOrder[]
list.so there is two impact here, first other users limit order got removed and the second is that contract storage had bad state and burned tokens get stock in the list.
scenario #2 where attacker steal contract/users funds by wrong profit calculation
initiateMarketOrder(lowMargin)
to create position with ID1 while the margin is low._safeMint()
would make external call and callonERC721Received()
function of attacker address.initId[ID1]
is not yet set for ID1.addToPosition(ID1, bigMargin)
to increase the margin of the position the_checkDelay()
check would pass because both actions are opening position.initId[ID1]
by callingposition.addToPosition()
and the value were be based on thenewMargin
.Position.mint()
function and code would setinitId[ID1]
based on old margin value.initId[ID1]
for attacker position would be very low which would causeaccInterest
to be very higher than it supposed to be for position(inPosition.trades()
function calculations ) and would cause_payout
value to be very high (inpnl()
function's calculations) and when attacker close position ID1 attacker would receive a lot more profit from it.so attacker created a position with a lot of profit by reentering the logics and manipulating calculation of the profits for the position.
there can be other scenarios possible to perform and damage the protocol or users because there is no reentrancy protection mechanism and attacker only need to bypass validity checks of functions.
Tools Used
VIM
Recommended Mitigation Steps
follow the check-effect-interaction pattern.