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: 71/131
Findings: 3
Award: $19.85
🌟 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
From documentation:
In the event that a borrower has finished utilising the funds for the purpose that the market was set up to facilitate (or if lenders are choosing not to withdraw their assets and the borrower is paying too much interest on assets that have been re-deposited to the market), the borrower can close a market at will. This is a special case of reducing the APR (with the associated increased reserve rate that accompanies it). When a market is closed, sufficient assets must be repaid to increase the reserve ratio to 100%, after which interest ceases to accrue and no further parameter adjustment or borrowing is possible. The only thing possible to do in a closed market is for the lenders to file withdrawal requests and exit.
But owner could not close market, because there is not function in WildcatMarketController, which could call market.closeMarket(). Borrower could not call market.closeMarket() directly, because this function has modifier onlyController, which allow calls only from WildcatMarketController contract
Just search phrase "closeMarket" in WildcatMarketController.sol file. You will not find it.
Manual review
Add function to WildcatMarketController.sol for borrower, which allow him call market.closeMarket()
Error
#0 - c4-pre-sort
2023-10-27T07:29:05Z
minhquanym marked the issue as duplicate of #147
#1 - c4-judge
2023-11-07T14:06:41Z
MarioPoneder marked the issue as partial-50
#2 - c4-judge
2023-11-07T14:16:53Z
MarioPoneder changed the severity to 3 (High Risk)
🌟 Selected for report: 0xStalin
Also found by: 0xCiphky, 0xComfyCat, 0xbepresent, 3docSec, Eigenvectors, Fulum, HChang26, Infect3d, QiuhaoLi, SandNallani, SovaSlava, TrungOre, YusSecurity, ZdravkoHr, ast3ros, ayden, bdmcbri, cu5t0mpeo, elprofesor, gizzy, jasonxiale, kodyvim, marqymarq10, max10afternoon, nisedo, nobody2018, radev_sw, rvierdiiev, serial-coder, xeros
13.1205 USDC - $13.12
If a lender knows that he is blacklisted in chainalysis Sanctions List and wants to withdraw tokens, but does not want them to go to an escrow contract, he can do:
function testPOCEscapeSanction() public { vm.startPrank(borrower); address[] memory lenders = new address[](2); lenders[0] = alice; lenders[1] = monika; controller.authorizeLenders(lenders); vm.stopPrank(); _deposit(alice, 1e18); _deposit(monika, 1e18); sanctionsSentinel.sanction(alice); vm.prank(alice); market.transfer(monika,1e18); vm.startPrank(monika); market.queueWithdrawal(1e18); uint256 expiry = block.timestamp + parameters.withdrawalBatchDuration; fastForward(parameters.withdrawalBatchDuration); market.executeWithdrawal(monika, uint32(expiry)); // creating escrow asset.transfer(alice, 1e18); }
Manual review
Add check in function transfer and transferFrom, that msg.sender dont included in chainalysisSanctionsList.
Error
#0 - c4-pre-sort
2023-10-27T03:24:00Z
minhquanym marked the issue as duplicate of #54
#1 - c4-judge
2023-11-07T14:36:22Z
MarioPoneder changed the severity to 3 (High Risk)
#2 - c4-judge
2023-11-07T14:40:37Z
MarioPoneder marked the issue as satisfactory
🌟 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/WildcatSanctionsSentinel.sol#L95-L99 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/market/WildcatMarketBase.sol#L172-L176
Due to the incorrect order of arguments in createEscrow function in WildcatSanctionsSentinel contract, lender's tokens/assets is sent directly to the borrower. The variable account
is assigned the borrower's address, while the variable borrower
is assigned the creditor's address.
Example:
The lender gets added to the chainalysis sanctions list, and someone calls the nukeFromOrbit() function. This function calls internal function _blockAccount(state, lendersAddress)
.
function _blockAccount(MarketState memory state, address accountAddress) internal { ... if (scaledBalance > 0) { account.scaledBalance = 0; // @audit-issue Wrong arguments order. address escrow = IWildcatSanctionsSentinel(sentinel).createEscrow( accountAddress, // <---- correct variable should be borrower borrower, // <---- correct variable should be accountAddress(lender) address(this) ); emit Transfer(accountAddress, escrow, state.normalizeAmount(scaledBalance)); _accounts[escrow].scaledBalance += scaledBalance; ... } } // createEscrow function function createEscrow( address borrower, <--- account's address was passed instead borrower's address account, <--- borrower's address was passed instead account's address asset ) public override returns (address escrowContract) { .... tmpEscrowParams = TmpEscrowParams(borrower, account, asset); new WildcatSanctionsEscrow{ salt: keccak256(abi.encode(borrower, account, asset)) }(); // constructor of WildcatSanctionsEscrow contract constructor() { sentinel = msg.sender; (borrower, account, asset) = WildcatSanctionsSentinel(sentinel).tmpEscrowParams(); }
After the escrow contract is created, anyone can call the WildcatSanctionsEscrow.releaseEscrow() function.
This function call internal function canReleaseEscrow() and check, that the return value of this function is always true. Function canReleaseEscrow()
sends borrower
and account
addresses to isSanctioned function. The problem is that the values in these variables are swapped with each other.
function canReleaseEscrow() public view override returns (bool) { // send lender's address as first argument! // send borrower's address as second argument! return !WildcatSanctionsSentinel(sentinel).isSanctioned(borrower, account); }
WildcatSanctionsSentinel.sol
function isSanctioned(address borrower, address account) public view override returns (bool) { return !sanctionOverrides[borrower][account] && IChainalysisSanctionsList(chainalysisSanctionsList).isSanctioned(account); }
This function will return false, because in reality, the check will be performed with incorrect addresses (because they are swapped with each other).
sanctionOverrides[borrower][account] -> sanctionOverrides[LENDER][BORROWER] -> False IChainalysisSanctionsList(chainalysisSanctionsList).isSanctioned(BORROWER) -> False False && False -> False
Look at releaseEscrow function
function releaseEscrow() public override { if (!canReleaseEscrow()) revert CanNotReleaseEscrow(); <------ canReleaseEscrow return always true uint256 amount = balance(); IERC20(asset).transfer(account, amount); <---- remember, in account variable stored borrower address emit EscrowReleased(account, asset, amount); }
So tokens will be transfered to borrower.
Create file transferFromEscrowToWrongAccount.sol in test/market/
// SPDX-License-Identifier: MIT pragma solidity >=0.8.20; import '../BaseMarketTest.sol'; import { WildcatSanctionsEscrow, IWildcatSanctionsEscrow } from 'src/WildcatSanctionsEscrow.sol'; contract WildcatMarketConfigTest is BaseMarketTest { event EscrowReleased(address indexed account, address indexed asset, uint256 amount); function test_transferFromEscrowToWrongAccount() public { _deposit(alice, 1e18); sanctionsSentinel.sanction(alice); address escrow = sanctionsSentinel.getEscrowAddress(alice, borrower, address(market)); market.nukeFromOrbit(alice); vm.expectEmit(address(escrow)); // Token transfers to borrower, not to Alice emit EscrowReleased(borrower, address(market), 1e18); IWildcatSanctionsEscrow(escrow).releaseEscrow(); } }
Manual review
Change order or function arguments in WildcatSanctionsSentinel.createEscrow()
function createEscrow( - address borrower, - address account, + address account, + address borrower address asset ) public override returns (address escrowContract) { if (!IWildcatArchController(archController).isRegisteredMarket(msg.sender)) { revert NotRegisteredMarket(); } ....
Other
#0 - c4-pre-sort
2023-10-27T02:45:08Z
minhquanym marked the issue as duplicate of #515
#1 - c4-judge
2023-11-07T12:07:22Z
MarioPoneder marked the issue as satisfactory