The WildcatMarketConfig::nukeFromOrbit() and the WildcatMarketWithdrawals::executeWithdrawal() create incorrect Escrows for locking the blocked lender's asset tokens (underlying assets) and market tokens.
Impact
A borrower can steal all asset tokens and market tokens of the blocked lender. Eventually, the borrower can burn the stolen market tokens and withdraw all underlying asset tokens from the market.
Proof of Concept
This PoC section is divided into three sub-sections:
Explaining how the nukeFromOrbit() creates an incorrect Escrow for the market tokens
Explaining how the executeWithdrawal() creates incorrect Escrows for the market tokens and the asset tokens
Step-by-step exploit scenarios
Explaining how the nukeFromOrbit() creates an incorrect Escrow for the market tokens
Once a lender gets sanctioned by Chainalysis, anyone can trigger the WildcatMarketConfig::nukeFromOrbit() to block the lender from interacting with the market and transfer all lender's market tokens to an Escrow contract.
Explaining how the executeWithdrawal() creates incorrect Escrows for the market tokens and the asset tokens
When a sanctioned lender (but has not been blocked from the market) executes the WildcatMarketWithdrawals::executeWithdrawal(), the function will invoke the WildcatMarketBase::_blockAccount() to block the lender from the market and then transfer all their market tokens to a created Escrow.
In this step, the _blockAccount() will create an incorrect Escrow for the blocked lender's market tokens, allowing the borrower to steal all locked market tokens. For a detailed explanation, please refer to the Explaining how the nukeFromOrbit() creates an incorrect Escrow for the market tokens section above.
Then, the executeWithdrawal() will execute the WildcatSanctionsSentinel::createEscrow() to create an Escrow for holding the blocked lender's asset tokens (from a pending withdrawal request that has expired). Similar to the _blockAccount(), the executeWithdrawal() will pass the accountAddress and borrower arguments into the createEscrow() alternately with its required parameters.
There are two exploit scenarios based on the sanctioned lender (victim)'s actions prior to the sanction event.
If the sanctioned lender (victim) has no pending withdrawal request:
A borrower (attacker) executes the WildcatMarketConfig::nukeFromOrbit() to block the lender and transfer all lender's market tokens to the incorrectly created Escrow.
The borrower invokes the WildcatSanctionsEscrow::releaseEscrow() on the Escrow to steal the locked market tokens. Because the account parameter will point to the borrower (who is not sanctioned by Chainalysis), the check for the release authorization by the canReleaseEscrow() will be passed (more refs: #1 and #2). The market tokens will be transferred to the borrower.
The borrower triggers the WildcatMarketWithdrawals::queueWithdrawal() to create a pending request for withdrawing (underlying) asset tokens by burning the stolen market tokens for exchange. As a result of Step 3, the borrower now becomes a legitimate lender with the approval status == AuthRole.DepositAndWithdraw. Therefore, the check for the withdrawal authorization by the _getAccountWithRole() will be passed.
If the sanctioned lender (victim) has previously executed some pending withdrawal requests before being sanctioned:
A borrower (attacker) executes the WildcatMarketWithdrawals::executeWithdrawal() to block the lender (if necessary) and transfer both the market tokens and underlying asset tokens (from a pending withdrawal request that has expired) of the blocked lender to the incorrectly created Escrows (There will be 2 Escrows -- one for the market tokens and another for the asset tokens).
The borrower invokes the WildcatSanctionsEscrow::releaseEscrow() on both Escrows to steal the locked market tokens and the underlying asset tokens. Because the account parameter will point to the borrower (who is not sanctioned by Chainalysis), the check for the release authorization by the canReleaseEscrow() will be passed (more refs: #1 and #2). Finally, both the market tokens and asset tokens will be transferred to the borrower. At this step, the borrower has successfully stolen certain asset tokens. To steal the remaining tokens, perform the next step.
To fix the vulnerability, swap the passing arguments (accountAddress and borrower) in the WildcatMarketBase::_blockAccount() and in the WildcatMarketWithdrawals::executeWithdrawal(), like the snippet below.
Lines of code
https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketConfig.sol#L79 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketBase.sol#L173-L174 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketBase.sol#L178 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L96-L97 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L108 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L110 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketWithdrawals.sol#L165 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketWithdrawals.sol#L167-L168 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketWithdrawals.sol#L171 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsEscrow.sol#L38
Vulnerability details
The
WildcatMarketConfig::nukeFromOrbit()
and theWildcatMarketWithdrawals::executeWithdrawal()
create incorrect Escrows for locking the blocked lender's asset tokens (underlying assets) and market tokens.Impact
A borrower can steal all asset tokens and market tokens of the blocked lender. Eventually, the borrower can burn the stolen market tokens and withdraw all underlying asset tokens from the market.
Proof of Concept
This PoC section is divided into three sub-sections:
Explaining how the nukeFromOrbit() creates an incorrect Escrow for the market tokens
Once a lender gets sanctioned by Chainalysis, anyone can trigger the
WildcatMarketConfig::nukeFromOrbit()
to block the lender from interacting with the market and transfer all lender's market tokens to an Escrow contract.The
nukeFromOrbit()
invokes theWildcatMarketBase::_blockAccount()
to block the lender. The_blockAccount()
will create the Escrow for holding the blocked lender's market tokens. However, the_blockAccount()
will pass theaccountAddress
andborrower
arguments into theWildcatSanctionsSentinel::createEscrow()
alternately with thecreateEscrow()
's required parameters.Subsequently, the
createEscrow()
'sborrower
parameter will point to the blocked lender, whereas theaccount
parameter will point to the borrower instead. Therefore, thecreateEscrow()
will create the incorrect Escrow contract.After that, all blocked lender's market tokens will be transferred to the incorrectly generated Escrow, allowing the borrower to execute the
WildcatSanctionsEscrow::releaseEscrow()
on the Escrow to steal all market tokens.Invoke the _blockAccount() to block the sanctioned lender
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketConfig.sol#L79The incorrect Escrow for the market tokens is created
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketBase.sol#L173-L174All blocked lender's market tokens are sent to the Escrow
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketBase.sol#L178Notice the difference between the function params and the passed arguments
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L96-L97Now, the borrower == address(blocked lender) and account == address(legit borrower)
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L108Create an incorrect Escrow that the (legit) borrower can execute Escrow::releaseEscrow() to steal the blocked lender's tokens
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L110Explaining how the executeWithdrawal() creates incorrect Escrows for the market tokens and the asset tokens
When a sanctioned lender (but has not been blocked from the market) executes the
WildcatMarketWithdrawals::executeWithdrawal()
, the function will invoke theWildcatMarketBase::_blockAccount()
to block the lender from the market and then transfer all their market tokens to a created Escrow.In this step, the
_blockAccount()
will create an incorrect Escrow for the blocked lender's market tokens, allowing the borrower to steal all locked market tokens. For a detailed explanation, please refer to theExplaining how the nukeFromOrbit() creates an incorrect Escrow for the market tokens
section above.Then, the
executeWithdrawal()
will execute theWildcatSanctionsSentinel::createEscrow()
to create an Escrow for holding the blocked lender's asset tokens (from a pending withdrawal request that has expired). Similar to the_blockAccount()
, theexecuteWithdrawal()
will pass theaccountAddress
andborrower
arguments into thecreateEscrow()
alternately with its required parameters.For this reason, the
createEscrow()
'sborrower
parameter will point to the blocked lender, whereas theaccount
parameter will point to the borrower unintentionally. Hence, thecreateEscrow()
will create the incorrect Escrow contract.Lastly, all blocked lender's asset tokens (from a pending withdrawal request that has expired) will be transferred to the incorrectly created Escrow, allowing the borrower to execute the
WildcatSanctionsEscrow::releaseEscrow()
on the Escrow to steal all asset tokens.Invoke the _blockAccount() to block the sanctioned lender
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketWithdrawals.sol#L165The incorrect Escrow for the asset tokens (underlying assets) is created
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketWithdrawals.sol#L167-L168All blocked lender's asset tokens (from a pending withdrawal request that has expired) are sent to the Escrow
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketWithdrawals.sol#L171The incorrect Escrow for the market tokens is created
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketBase.sol#L173-L174All blocked lender's market tokens are sent to the Escrow
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketBase.sol#L178Notice the difference between the function params and the passed arguments
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L96-L97Now, the borrower == address(blocked lender) and account == address(legit borrower)
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L108Create an incorrect Escrow that the (legit) borrower can execute Escrow::releaseEscrow() to steal the blocked lender's tokens
: https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L110Step-by-step exploit scenarios
There are two exploit scenarios based on the sanctioned lender (victim)'s actions prior to the sanction event.
If the sanctioned lender (victim) has no pending withdrawal request:
A borrower (attacker) executes the
WildcatMarketConfig::nukeFromOrbit()
to block the lender and transfer all lender's market tokens to the incorrectly created Escrow.The borrower invokes the
WildcatSanctionsEscrow::releaseEscrow()
on the Escrow to steal the locked market tokens. Because theaccount
parameter will point to the borrower (who is not sanctioned by Chainalysis), the check for the release authorization by thecanReleaseEscrow()
will be passed (more refs: #1 and #2). The market tokens will be transferred to the borrower.The borrower executes the
WildcatMarketController::authorizeLenders()
to authorize themselves as a lender and then calls theWildcatMarketController::updateLenderAuthorization()
to apply the lender authorization to the target market.The borrower triggers the
WildcatMarketWithdrawals::queueWithdrawal()
to create a pending request for withdrawing (underlying) asset tokens by burning the stolen market tokens for exchange. As a result of Step 3, the borrower now becomes a legitimate lender with the approval status ==AuthRole.DepositAndWithdraw
. Therefore, the check for the withdrawal authorization by the_getAccountWithRole()
will be passed.Once the pending withdrawal request has expired, the borrower triggers the
WildcatMarketWithdrawals::executeWithdrawal()
to withdraw the blocked lender's (underlying) asset tokens from the market. The stolen asset tokens will be transferred to the borrower.If the sanctioned lender (victim) has previously executed some pending withdrawal requests before being sanctioned:
A borrower (attacker) executes the
WildcatMarketWithdrawals::executeWithdrawal()
to block the lender (if necessary) and transfer both the market tokens and underlying asset tokens (from a pending withdrawal request that has expired) of the blocked lender to the incorrectly created Escrows (There will be 2 Escrows -- one for the market tokens and another for the asset tokens).The borrower invokes the
WildcatSanctionsEscrow::releaseEscrow()
on both Escrows to steal the locked market tokens and the underlying asset tokens. Because theaccount
parameter will point to the borrower (who is not sanctioned by Chainalysis), the check for the release authorization by thecanReleaseEscrow()
will be passed (more refs: #1 and #2). Finally, both the market tokens and asset tokens will be transferred to the borrower. At this step, the borrower has successfully stolen certain asset tokens. To steal the remaining tokens, perform the next step.The borrower must burn the stolen market tokens for the underlying asset tokens via the process of executing the
WildcatMarketWithdrawals::queueWithdrawal()
andWildcatMarketWithdrawals::executeWithdrawal()
similar to Steps 1.3 - 1.5 above.Tools Used
Manual Review
Recommended Mitigation Steps
To fix the vulnerability, swap the passing arguments (
accountAddress
andborrower
) in theWildcatMarketBase::_blockAccount()
and in theWildcatMarketWithdrawals::executeWithdrawal()
, like the snippet below.Assessed type
Other