Platform: Code4rena
Start Date: 16/10/2023
Pot Size: $60,500 USDC
Total HM: 16
Participants: 131
Period: 10 days
Judge: 0xTheC0der
Total Solo HM: 3
Id: 296
League: ETH
Rank: 97/131
Findings: 2
Award: $6.73
π Selected for report: 0
π Solo Findings: 0
π Selected for report: 0xpiken
Also found by: 0xCiphky, 0xComfyCat, 0xStalin, 0xhegel, 0xkazim, 3docSec, AM, Aymen0909, CaeraDenoir, DeFiHackLabs, Drynooo, Eigenvectors, Fulum, HALITUS, HChang26, Jiamin, Juntao, LokiThe5th, Mike_Bello90, MiloTruck, QiuhaoLi, Silvermist, SovaSlava, SpicyMeatball, T1MOH, Toshii, TrungOre, TuringConsulting, Vagner, Yanchuan, ZdravkoHr, _nd_koo, almurhasan, audityourcontracts, ayden, cartlex_, circlelooper, crunch, cu5t0mpeo, deth, erictee, ggg_ttt_hhh, gizzy, gumgumzum, hash, jasonxiale, josephdara, ke1caM, kodyvim, lanrebayode77, marqymarq10, max10afternoon, nirlin, nonseodion, osmanozdemir1, peter, radev_sw, rvierdiiev, said, serial-coder, sl1, smiling_heretic, squeaky_cactus, stackachu, tallo, trachev, zaevlad
0.0606 USDC - $0.06
Judge has assessed an item in Issue #369 as 3 risk. The relevant finding follows:
L-01 Controller canβt call setMaxTotalSupply due to lack of function to do it.
#0 - c4-judge
2023-11-09T15:52:27Z
MarioPoneder marked the issue as duplicate of #431
#1 - c4-judge
2023-11-09T15:52:33Z
MarioPoneder marked the issue as satisfactory
#2 - c4-judge
2023-11-09T15:52:38Z
MarioPoneder marked the issue as partial-50
π Selected for report: YusSecurity
Also found by: 0xAsen, 0xCiphky, 0xDING99YA, 0xKbl, 0xSwahili, 0xbepresent, 3docSec, AS, Aymen0909, DeFiHackLabs, GREY-HAWK-REACH, KeyKiril, MiloTruck, QiuhaoLi, Silvermist, SovaSlava, TrungOre, VAD37, Vagner, Yanchuan, ZdravkoHr, ast3ros, cartlex_, d3e4, deth, ggg_ttt_hhh, gizzy, kodyvim, nirlin, nobody2018, rvierdiiev, serial-coder, sl1, tallo, xeros
6.6715 USDC - $6.67
https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketWithdrawals.sol#L137-L188 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketWithdrawals.sol#L166-L170 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L95-L99 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsEscrow.sol#L33-L41 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/WildcatSanctionsSentinel.sol#L39-L43 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarketBase.sol#L173-L175
Lender unexpectedly can transfer funds to the borrower instead of itself and lost them.
When lender want to withdraw underlying asset from the market
contract executeWithdrawal()
function can be used.
However, if lender have been added to the sanction list, amount of funds that lender want to withdraw transfers to the escrow
contract, which is creating inside executeWithdrawal()
using createEscrow()
function.
... if (IWildcatSanctionsSentinel(sentinel).isSanctioned(borrower, accountAddress)) { _blockAccount(state, accountAddress); address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( accountAddress, borrower, address(asset) ); asset.safeTransfer(escrow, normalizedAmountWithdrawn); emit SanctionedAccountWithdrawalSentToEscrow( accountAddress, escrow, expiry, normalizedAmountWithdrawn ); } else { asset.safeTransfer(accountAddress, normalizedAmountWithdrawn); } ...
createEscrow()
takes as arguments borrower
, account
and asset
and has the following order.
function createEscrow( address borrower, address account, address asset ) public override returns (address escrowContract) {
You can see that the order of arguments is different from executeWithdrawal()
.
As a consequence we have the next situation, that inside WildcatSanctionsEscrow.sol
contract borrower == account
, account == borrower
, asset == address(market)
.
It's possible to release funds that was blocked on WildcatSanctionsEscrow.sol
contract using releaseEscrow()
function.
function releaseEscrow() public override { if (!canReleaseEscrow()) revert CanNotReleaseEscrow(); uint256 amount = balance(); IERC20(asset).transfer(account, amount); emit EscrowReleased(account, asset, amount); }
Inside releaseEscrow()
the canReleaseEscrow()
function must return true
so that the user can return funds back. In turn, the function canReleaseEscrow()
checks wheter the account is sanctioned using isSanctioned()
function.
function isSanctioned(address borrower, address account) public view override returns (bool) { return !sanctionOverrides[borrower][account] && IChainalysisSanctionsList(chainalysisSanctionsList).isSanctioned(account); }
As I wrote earlier to complete the condition in releaseEscrow()
we must have false
as return value of isSanctioned()
function, in order to fulfill this condition, it is sufficient that the lender is not sanctioned or overrided by the borrower.
Since account == borrower
and vice versa the isSanctioned()
function returns false
.
Since that, account
can use releaseEscrow()
function, but funds will be transferred to the borrower
, since borrower == account
.
Here is the link to gist with Proof of concept which is caused by this flaw.
// SPDX-License-Identifier: MIT pragma solidity >=0.8.20; import "../src/interfaces/IMarketEventsAndErrors.sol"; import "./BaseMarketTest.sol"; interface ISanction { function addToSanctionsList(address[] memory newSanctions) external; function sanction(address account) external; function unsanction(address account) external; } interface IEscrow { function releaseEscrow() external; function balance() external view returns (uint256); } /** * To run the test: * 1. Create a <folder> where you can place initial files. * 2. Copy repository using `git clone https://github.com/code-423n4/2023-10-wildcat.git <folder>`. * 3. Copy `WildcatMarketConfig.high-02.t.sol` file from gist into foundry `test` folder. * 4. Use `forge test --mp test/WildcatMarketConfig.high-02.t.sol -vvvvv`. */ contract WildcatMarketConfigTest02 is BaseMarketTest { address public hole = makeAddr("hole"); // random address to burn token from Alice. function test_LenderCanUnblockItselfUseReleaseEscrow() external { vm.startPrank(alice); uint256 aliceStartBalance = asset.balanceOf(alice); uint256 aliceDepositAmount = 50_000e18; uint256 aliceWithdrawAmount = 35_000e18; // Preparation: Burn initial amount of tokens that Alice have, remain only 50_000 for better visualization in the test. asset.transfer(hole, aliceStartBalance); uint256 aliceStartBalanceAfterBurning = asset.balanceOf(alice); assertEq(aliceStartBalanceAfterBurning, 0); vm.stopPrank(); _deposit(alice, aliceDepositAmount); _requestWithdrawal(alice, aliceWithdrawAmount); uint256 expiry = block.timestamp + parameters.withdrawalBatchDuration; fastForward(parameters.withdrawalBatchDuration); MarketState memory state = pendingState(); updateState(state); // Block Alice address via chainalysis. address chainalysis = sanctionsSentinel.chainalysisSanctionsList(); ISanction(chainalysis).sanction(alice); // Alice use `overrideSanction()` function with borrower's address. vm.startPrank(alice); market.executeWithdrawal(alice, uint32(expiry)); sanctionsSentinel.overrideSanction(borrower); address escrowContractAddress = sanctionsSentinel.getEscrowAddress(alice, borrower, address(asset)); bool isAliceSanctionedAfter = sanctionsSentinel.isSanctioned(alice, borrower); assertEq(isAliceSanctionedAfter, false); uint256 assetAliceBalanceBeforeEscrowing = asset.balanceOf(alice); uint256 assetBorrowerBalanceBeforeEscrowing = asset.balanceOf(borrower); assertEq(asset.balanceOf(borrower), 0); emit log_named_decimal_uint("Alice's balance of asset before `releaseEscrow()` function was used", assetAliceBalanceBeforeEscrowing, 18); emit log_named_decimal_uint("Borrower's balance of asset before `releaseEscrow()` function was used", assetBorrowerBalanceBeforeEscrowing, 18); console.log(StdStyle.magenta("===================================================================================================")); // Alice can use `releaseEscrow()` function to get her funds back... IEscrow(escrowContractAddress).releaseEscrow(); // ...but funds were transferred to Borrower's address instead. uint256 assetAliceBalanceAfterEscrowing = asset.balanceOf(alice); assertEq(assetAliceBalanceAfterEscrowing, 0); uint256 assetBorrowerBalanceAfterEscrowing = asset.balanceOf(borrower); assertEq(assetBorrowerBalanceAfterEscrowing, aliceWithdrawAmount); emit log_named_decimal_uint("Alice's balance of asset after `releaseEscrow()` function was used", assetAliceBalanceAfterEscrowing, 18); emit log_named_decimal_uint("Borrowers's balance of asset after `releaseEscrow()` function was used", assetBorrowerBalanceAfterEscrowing, 18); console.log(StdStyle.magenta("===================================================================================================")); console.log(StdStyle.magenta("Alice used `releaseEscrow()` function, but funds were transferred to the borrower")); vm.stopPrank(); } }
Same flaw can be found in _blockAccount()
function. When it calling createEscrow()
function internally, the next arguments orderring is used:
address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( accountAddress, // @audit invalid order of arguments borrower, address(this) );
Manual review, foundry.
Consider to change the order of input arguments in executeWithdrawal()
and _blockAccount()
functions when createEscrow()
function is used.
address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( -- accountAddress, -- borrower, address(asset) );
address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( ++ borrower, ++ accountAddress, address(asset) );
Error
#0 - c4-pre-sort
2023-10-27T02:33:44Z
minhquanym marked the issue as duplicate of #515
#1 - c4-judge
2023-11-07T12:05:36Z
MarioPoneder marked the issue as satisfactory