Polynomial Protocol contest - btk's results

The DeFi Derivatives Powerhouse.

General Information

Platform: Code4rena

Start Date: 13/03/2023

Pot Size: $72,500 USDC

Total HM: 33

Participants: 35

Period: 7 days

Judge: Dravee

Total Solo HM: 16

Id: 222

League: ETH

Polynomial Protocol

Findings Distribution

Researcher Performance

Rank: 13/35

Findings: 1

Award: $956.38

QA:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Labels

bug
grade-a
QA (Quality Assurance)
Q-05

Awards

956.382 USDC - $956.38

External Links

Total Low issues
RiskIssues DetailsNumber
[L-01]Critical changes should use-two step procedure2
[L-02]Use safeMint instead of mint for ERC7211
[L-03]The first timestamp should be based on when funding rate was last updated1
[L-04]Integer overflow by unsafe casting1
[L-05]Missing checks for address(0)1
[L-06]Use require() instead of assert()2
[L-07]Lack of event emit3
[L-08]Fees are not capped3
[L-09]The nonReentrant modifier should occur before all other modifiers21
[L-10]Loss of precision due to rounding2
[L-11]Value is not validated to be different than the existing one20
[L-12]ShortToken implmentation is not fully up to EIP-721's specification1
[L-13]Avoid shadowing inherited state variables2
[L-14]Lack of access control in setVault() function leave it vulnerable to frontrunning attack1
Total Non-Critical issues
RiskIssues DetailsNumber
[NC-01]Include return parameters in NatSpec commentsAll Contracts
[NC-02]Function writing does not comply with the Solidity Style GuideAll Contracts
[NC-03]Mark visibility of init() functions as external1
[NC-04]Reusable require statements should be changed to a modifier3
[NC-05]The protocol should include NatSpecAll Contracts
[NC-06]Constants in comparisons should appear on the left side37
[NC-07]Use a more recent version of solidityAll Contracts
[NC-08]Contracts should have full test coverageAll Contracts
[NC-09]Add a timelock to critical functions12
[NC-10]Need Fuzzing testAll Contracts
[NC-11]Lock pragmas to specific compiler versionAll Contracts
[NC-12]Consider using delete rather than assigning zero to clear values11
[NC-13]For functions, follow Solidity standard naming conventions4
[NC-14]Events that mark critical parameter changes should contain both the old and the new value12
[NC-15]Add NatSpec Mapping comment8
[NC-16]Use SMTChecker

[L-01] Critical changes should use-two step procedure

Description

The following contracts (KangarooVault, LiquidityPool) have a function that allows them to change the feeReceipient to a different address. If the sender accidentally uses an invalid address for which they do not have the private key, then the protocol will lose fees.

Lines of code
    function setFeeReceipient(address _feeReceipient) external requiresAuth {
        feeReceipient = _feeReceipient;
    }
    function setFeeReceipient(address _feeReceipient) external requiresAuth {
        require(_feeReceipient != address(0x0));

        emit UpdateFeeReceipient(feeReceipient, _feeReceipient);

        feeReceipient = _feeReceipient;
    }

Consider adding two step procedure on the critical functions where the first is announcing a pending fee receipient and the new address should then claim its ownership.

[L-02] Use safeMint instead of mint for ERC721

Description

Users could lost their NFTs if msg.sender is a contract address that does not support ERC721, the NFT can be frozen in the contract forever.

As per the documentation of EIP-721:

A wallet/broker/auction application MUST implement the wallet interface if it will accept safe transfers.

Ref: https://eips.ethereum.org/EIPS/eip-721

As per the documentation of ERC721.sol by Openzeppelin:

Ref: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L274-L285

Lines of code
            _mint(trader, positionId);

Use _safeMint instead of mint to check received address support for ERC721 implementation.

[L-03] The first timestamp should be based on when funding rate was last updated

Description

The first timestamp should be based on when funding rate was last updated, so that an appropriate duration of the cycle occurs, rather than during deployment.

Lines of code
    uint256 public fundingLastUpdated = block.timestamp;

[L-04] Integer overflow by unsafe casting

Description

Keep in mind that the version of solidity used, despite being greater than 0.8, does not prevent integer overflows during casting, it only does so in mathematical operations.

It is necessary to safely convert between the different numeric types.

Lines of code
        uint256 newNormalizationFactor = normalizationFactor.mulWadDown(uint256(normalizationUpdate));
        normalizationFactor = normalizationFactor.mulWadDown(uint256(normalizationUpdate));
            uint256 marginWithdrawing = uint256(-marginDelta);
            uint256 marginAdding = uint256(marginDelta);
        positionData.lastSizeDelta = uint256(int256(position.size));
        uint256 currentSize = uint256(int256(position.size));
        positionData.lastSizeDelta = uint256(int256(position.size));
        uint256 currentSize = uint256(int256(position.size));
            uint256 availableFunds = uint256(int256(totalFunds) - usedFunds);
        totalValue -= uint256((int256(amountOwed) + usedFunds));
        require(usedFunds <= 0 || totalFunds >= uint256(usedFunds));
        require(usedFunds <= 0 || totalFunds >= uint256(usedFunds));
        require(usedFunds <= 0 || totalFunds >= uint256(usedFunds));
        return uint256(signedAbs(x));

Use OpenZeppelin safeCast library.

[L-05] Missing checks for address(0)

Description

Check of address(0) to protect the code from (0x0000000000000000000000000000000000000000) address problem just in case. This is best practice or instead of suggesting that they verify _address != address(0), you could add some good NatSpec comments explaining what is valid and what is invalid and what are the implications of accidentally using an invalid address.

Lines of code
    function setFeeReceipient(address _feeReceipient) external requiresAuth {

Add checks for address(0) when assigning values to address state variables.

[L-06] Use require() instead of assert()

Description

Assert should not be used except for tests, require should be used

Prior to Solidity 0.8.0, pressing a confirm consumes the remainder of the process's available gas instead of returning it, as request()/revert() did.

The big difference between the two is that the assert() function when false, uses up all the remaining gas and reverts all the changes made.

Meanwhile, a require() statement when false, also reverts back all the changes made to the contract but does refund all the remaining gas fees we offered to pay. This is the most common Solidity function used by developers for debugging and error handling.

Assertion should be avoided even after solidity version 0.8.0, because its documentation states "The Assert function generates an error of type Panic(uint256). Code that works properly should never Panic, even on invalid external input. If this happens, you need to fix it in your contract. there's a mistake".

Lines of code
        assert(queuedDepositHead + count - 1 < nextQueuedDepositId);
        assert(queuedWithdrawalHead + count - 1 < nextQueuedWithdrawalId);

Use require() instead of assert()

[L-07] Lack of event emit

Description

The below methods do not emit an event when the state changes, something that it's very important for dApps and users.

Lines of code
    function setFeeReceipient(address _feeReceipient) external requiresAuth {
        feeReceipient = _feeReceipient;
    }
    function setVault(address _vault) external {
        if (vault != address(0x0)) {
            revert();
        }
        vault = _vault;
    }
    receive() external payable {
        (bool success,) = feeReceipient.call{value: msg.value}("");
        require(success);
    }

Emit event.

[L-08] Fees are not capped

Description

Fees are not capped, which makes the protocol less decentralized. Thus, increasing the likelihood of losing trust from users.

Lines of code
    function setFees(uint256 _depositFee, uint256 _withdrawalFee) external requiresAuth {
        emit UpdateFees(depositFee, _depositFee, withdrawalFee, _withdrawalFee);
        depositFee = _depositFee;
        withdrawalFee = _withdrawalFee;
    }
    function setBaseTradingFee(uint256 _baseTradingFee) external requiresAuth {
        emit UpdateBaseTradingFee(baseTradingFee, _baseTradingFee);
        baseTradingFee = _baseTradingFee;
    }
    function setDevFee(uint256 _devFee) external requiresAuth {
        emit UpdateDevFee(devFee, _devFee);
        devFee = _devFee;
    }

[L-09] The nonReentrant modifier should occur before all other modifiers

Description

This is a best-practice to protect against reentrancy in other modifiers.

Lines of code
    function openPosition(uint256 amt, uint256 minCost) external requiresAuth nonReentrant {
    function closePosition(uint256 amt, uint256 maxCost) external requiresAuth nonReentrant {
    function clearPendingOpenOrders(uint256 maxCost) external requiresAuth nonReentrant {
    function clearPendingCloseOrders(uint256 minCost) external requiresAuth nonReentrant {
    function transferPerpMargin(int256 marginDelta) external requiresAuth nonReentrant {
    function addCollateral(uint256 additionalCollateral) external requiresAuth nonReentrant {
    function removeCollateral(uint256 collateralToRemove) external requiresAuth nonReentrant {
    function executePerpOrders(bytes[] calldata priceUpdateData) external payable requiresAuth nonReentrant {
    function openLong(uint256 amount, address user, bytes32 referralCode)
        external
        override
        onlyExchange
        nonReentrant
        returns (uint256 totalCost)
    {
    function closeLong(uint256 amount, address user, bytes32 referralCode)
        external
        override
        onlyExchange
        nonReentrant
        returns (uint256 totalCost)
    {
    function openShort(uint256 amount, address user, bytes32 referralCode)
        external
        override
        onlyExchange
        nonReentrant
        returns (uint256 totalCost)
    {
    function closeShort(uint256 amount, address user, bytes32 referralCode)
        external
        override
        onlyExchange
        nonReentrant
        returns (uint256 totalCost)
    {
    function liquidate(uint256 amount) external override onlyExchange nonReentrant {
    function hedgePositions() external override requiresAuth nonReentrant {
    function rebalanceMargin(int256 marginDelta) external requiresAuth nonReentrant {
    function increaseMargin(uint256 additionalMargin) external requiresAuth nonReentrant {
    function placeQueuedOrder() external requiresAuth nonReentrant {
    function executePerpOrders(bytes[] calldata priceUpdateData) external payable requiresAuth nonReentrant {
    function collectCollateral(address collateral, uint256 positionId, uint256 amount)
        external
        onlyExchange
        nonReentrant
    {
    function sendCollateral(uint256 positionId, uint256 amount) external override onlyExchange nonReentrant {
    function liquidate(uint256 positionId, uint256 debt, address user)
        external
        override
        onlyExchange
        nonReentrant
        returns (uint256 totalCollateralReturned)
    {

Use the nonReentrant modifier first.

[L-10] Loss of precision due to rounding

Description

Loss of precision due to the nature of arithmetics and rounding errors.

Lines of code
        uint256 region = size / standardSize;
        maxDebt = position.shortAmount / 2;

[L-11] Value is not validated to be different than the existing one

Description

Value is not validated to be different than the existing one. Queueing the same value will cause multiple abnormal events to be emitted, will ultimately result in a no-op situation.

Lines of code
    function setSkewNormalizationFactor(uint256 _skewNormalizationFactor) external requiresAuth {
    function setMaxFundingRate(uint256 _maxFundingRate) external requiresAuth {
    function setFeeReceipient(address _feeReceipient) external requiresAuth {
    function setFees(uint256 _depositFee, uint256 _withdrawalFee) external requiresAuth {
    function setBaseTradingFee(uint256 _baseTradingFee) external requiresAuth {
    function setDevFee(uint256 _devFee) external requiresAuth {
    function setPerpPriceImpactDelta(uint256 _perpPriceImpactDelta) external requiresAuth {
    function setMinDelays(uint256 _minDepositDelay, uint256 _minWithdrawDelay) external requiresAuth {
    function setStatusFunction(bytes32 key, bool status) public requiresAuth {
    function setFeeReceipient(address _feeReceipient) external requiresAuth {
    function setFees(uint256 _performanceFee, uint256 _withdrawalFee) external requiresAuth {
    function setSynthetixTracking(bytes32 _code) external requiresAuth {
    function setReferralCode(bytes32 _code) external requiresAuth {
    function setMinDepositAmount(uint256 _minAmt) external requiresAuth {
    function setMaxDepositAmount(uint256 _maxAmt) external requiresAuth {
    function setDelays(uint256 _depositDelay, uint256 _withdrawDelay) external requiresAuth {
    function setPriceImpactDelta(uint256 _delta) external requiresAuth {
    function setLeverage(uint256 _lev) external requiresAuth {
    function setCollRatio(uint256 _ratio) external requiresAuth {

Add a require() statement to check that the new value is different than the current one.

[L-12] ShortToken implmentation is not fully up to EIP-721's specification

Description

tokenURI() return an empty string which could cause unexpected behavior in the future due to non-compliance with EIP-721 standard.

Lines of code
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        return "";
    }

tokenURI() should return something and throws if tokenId is not a valid NFT.

[L-13] Avoid shadowing inherited state variables

Description

In VaultToken.sol there is a local variables named name symbol, but there is state variables in the inherited contract ( ERC20.sol) with the same name. This use causes compilers to issue warnings, negatively affecting checking and code readability.

Lines of code
    constructor(string memory name, string memory symbol) ERC20(name, symbol, 18) {}

Avoid using variables with the same name.

[L-14] Lack of access control in setVault() function leave it vulnerable to frontrunning attack

Description

setVault() function has no access control and can be called by anyone which leave it vulnerable to frontrunning attack, and since this function can only be set once, thus it may force a redeployment.

Lines of code
    function setVault(address _vault) external {
        if (vault != address(0x0)) {
            revert();
        }
        vault = _vault;
    }

Add access control to setVault() to protect the function and make more robust.

[NC-01] Include return parameters in NatSpec comments

Description

If Return parameters are declared, you must prefix them with /// @return. Some code analysis programs do analysis by reading NatSpec details, if they can't see the @return tag, they do incomplete analysis.

Lines of code

Include @return parameters in NatSpec comments

[NC-02] Function writing does not comply with the Solidity Style Guide

Description

Ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.

Functions should be grouped according to their visibility and ordered:

  • constructor()
  • receive()
  • fallback()
  • external / public / internal / private
  • view / pure
Lines of code

Follow Solidity Style Guide.

[NC-03] Mark visibility of init() functions as external

Description
  • If someone wants to extend via inheritance, it might make more sense that the overridden init() function calls the internal {...}_init function, not the parent public init() function.

  • External instead of public would give more the sense of the init() functions to behave like a constructor (only called on deployment, so should only be called externally)

  • Security point of view, it might be safer so that it cannot be called internally by accident in the child contract

  • It might cost a bit less gas to use external over public

  • It is possible to override a function from external to public ("opening it up") but it is not possible to override a function from public to external ("narrow it down").

Ref: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3750

Lines of code
    function init(
        address _pool,
        address _powerPerp,
        address _exchange,
        address _liquidityToken,
        address _shortToken,
        address _synthetixAdapter,
        address _shortCollateral
    ) public {

Change the visibility of init() functions to external

[NC-04] Reusable require statements should be changed to a modifier

Description

Reusable require statements should be changed to a modifier.

Lines of code
        require(powerPerp.balanceOf(_to) == 0, "Receiver has long positions");
        require(powerPerp.balanceOf(_to) == 0, "Receiver has long positions");
        require(powerPerp.balanceOf(_to) == 0, "Receiver has long positions");

[NC-05] The protocol should include NatSpec

Description

It is recommended that Solidity contracts are fully annotated using NatSpec, it is clearly stated in the Solidity official documentation.

  • In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.

  • Some code analysis programs do analysis by reading NatSpec details, if they can't see the tags (@param, @dev, @return), they do incomplete analysis.

Lines of code

Include NatSpec comments in the codebase.

[NC-06] Constants in comparisons should appear on the left side

Description

Constants in comparisons should appear on the left side, doing so will prevent typo bug.

            require(holdings == 0, "Shouldn't have long positions to close short postions");
Lines of code
            require(shortPositions == 0, "Short position must be closed before opening");
            require(holdings == 0, "Long position must be closed before opening");
            require(shortPositions == 0, "Shouldn't have short positions to close long postions");
            require(holdings == 0, "Shouldn't have long positions to close short postions");
        if (positionData.positionId == 0) {
        if (positionData.positionId == 0) {
            if (current.requestedTime == 0 || block.timestamp < current.requestedTime + minDepositDelay) {
            if (current.requestedTime == 0 || block.timestamp < current.requestedTime + minWithdrawDelay) {
            if (availableFunds == 0) {
        if (totalFunds == 0) {
        if (positionData.positionId == 0) {
        require(positionData.pendingLongPerp == 0 && positionData.pendingShortPerp == 0);
        require(delayedOrder.sizeDelta == 0);
        if (positionData.positionId == 0) {
        require(positionData.pendingLongPerp > 0 && positionData.pendingShortPerp == 0);
        require(delayedOrder.sizeDelta == 0);
            if (positionData.shortAmount == 0) {
        require(positionData.positionId != 0 && positionData.pendingLongPerp == 0 && positionData.pendingShortPerp == 0);
        require(delayedOrder.sizeDelta == 0);
        require(positionData.pendingLongPerp == 0 && positionData.pendingShortPerp > 0);
        require(delayedOrder.sizeDelta == 0);
            if (positionData.shortAmount == 0) {
            if (current.requestedTime == 0 || block.timestamp < current.requestedTime + minDepositDelay) {
            if (current.requestedTime == 0 || block.timestamp < current.requestedTime + minWithdrawDelay) {
            if (availableFunds == 0) {
        if (totalFunds == 0) {
        if (skew == 0) {
        require(order.sizeDelta == 0);
        require(shortToken.balanceOf(_to) == 0, "Receiver has short positions");
        require(shortToken.balanceOf(_to) == 0, "Receiver has short positions");
        if (positionId == 0) {
            if (position.shortAmount == 0) {
        require(powerPerp.balanceOf(_to) == 0, "Receiver has long positions");
        require(powerPerp.balanceOf(_to) == 0, "Receiver has long positions");
        require(powerPerp.balanceOf(_to) == 0, "Receiver has long positions");

Constants should appear on the left side:

            require(0 == holdings, "Shouldn't have long positions to close short postions");

[NC-07] Use a more recent version of solidity

Description

For security, it is best practice to use the latest Solidity version.

Lines of code

Old version of Solidity is used (^0.8.9), newer version can be used (0.8.19).

[NC-08] Contracts should have full test coverage

Description

While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.

Lines of code
- What is the overall line coverage percentage provided by your tests?: 85

Line coverage percentage should be 100%.

[NC-09] Add a timelock to critical functions

Description

It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate.

Lines of code
    function setMaxFundingRate(uint256 _maxFundingRate) external requiresAuth {
    function setFees(uint256 _depositFee, uint256 _withdrawalFee) external requiresAuth {
    function setBaseTradingFee(uint256 _baseTradingFee) external requiresAuth {
    function setDevFee(uint256 _devFee) external requiresAuth {
    function setPerpPriceImpactDelta(uint256 _perpPriceImpactDelta) external requiresAuth {
    function setMinDelays(uint256 _minDepositDelay, uint256 _minWithdrawDelay) external requiresAuth {
    function setFees(uint256 _performanceFee, uint256 _withdrawalFee) external requiresAuth {
    function setMinDepositAmount(uint256 _minAmt) external requiresAuth {
    function setMaxDepositAmount(uint256 _maxAmt) external requiresAuth {
    function setDelays(uint256 _depositDelay, uint256 _withdrawDelay) external requiresAuth {
    function setPriceImpactDelta(uint256 _delta) external requiresAuth {
    function setCollRatio(uint256 _ratio) external requiresAuth {

Consider adding a timelock to the critical changes.

[NC-10] Need Fuzzing test

Description

As Alberto Cuesta Canada said: Fuzzing is not easy, the tools are rough, and the math is hard, but it is worth it. Fuzzing gives me a level of confidence in my smart contracts that I didn’t have before. Relying just on unit testing anymore and poking around in a testnet seems reckless now.

Ref: https://medium.com/coinmonks/smart-contract-fuzzing-d9b88e0b0a05

Lines of code

Use should fuzzing test like Echidna.

[NC-11] Lock pragmas to specific compiler version

Description

Pragma statements can be allowed to float when a contract is intended for consumption by other developers, as in the case with contracts in a library or EthPM package. Otherwise, the developer would need to manually update the pragma in order to compile locally.

Ref: https://swcregistry.io/docs/SWC-103

Lines of code

Ethereum Smart Contract Best Practices: Lock pragmas to specific compiler version.

[NC-12] Consider using delete rather than assigning zero to clear values

Description

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.

Lines of code
            current.depositedAmount = 0;
                current.withdrawnTokens = 0;
            current.depositedAmount = 0;
                current.withdrawnTokens = 0;
                positionData.premiumCollected = 0;
        positionData.pendingLongPerp = 0;
            positionData.premiumCollected = 0;
        positionData.pendingShortPerp = 0;
        positionData.positionId = 0;
        positionData.premiumCollected = 0;
        positionData.totalMargin = 0;

Use the delete keyword.

[NC-13] For functions, follow Solidity standard naming conventions

Description

The protocol don't follow solidity standard naming convention.

Ref: https://docs.soliditylang.org/en/v0.8.17/style-guide.html#naming-conventions

Lines of code
    function signedAbs(int256 x) internal pure returns (int256) {
    function abs(int256 x) internal pure returns (uint256) {
    function max(int256 x, int256 y) internal pure returns (int256) {
    function min(int256 x, int256 y) internal pure returns (int256) {

Follow solidity standard naming convention.

[NC-14] Events that mark critical parameter changes should contain both the old and the new value

Description

Events that mark critical parameter changes should contain both the old and the new value.

Lines of code
    function setMaxFundingRate(uint256 _maxFundingRate) external requiresAuth {
    function setFees(uint256 _depositFee, uint256 _withdrawalFee) external requiresAuth {
    function setBaseTradingFee(uint256 _baseTradingFee) external requiresAuth {
    function setDevFee(uint256 _devFee) external requiresAuth {
    function setPerpPriceImpactDelta(uint256 _perpPriceImpactDelta) external requiresAuth {
    function setMinDelays(uint256 _minDepositDelay, uint256 _minWithdrawDelay) external requiresAuth {
    function setFees(uint256 _performanceFee, uint256 _withdrawalFee) external requiresAuth {
    function setMinDepositAmount(uint256 _minAmt) external requiresAuth {
    function setMaxDepositAmount(uint256 _maxAmt) external requiresAuth {
    function setDelays(uint256 _depositDelay, uint256 _withdrawDelay) external requiresAuth {
    function setPriceImpactDelta(uint256 _delta) external requiresAuth {
    function setCollRatio(uint256 _ratio) external requiresAuth {

Add the old value to the event.

[NC-15] Add NatSpec Mapping comment

Description

Add NatSpec comments describing mapping keys and values.

    mapping(bytes32 => bool) public isPaused;
Lines of code
    mapping(uint256 => QueuedDeposit) public depositQueue;
    mapping(uint256 => QueuedWithdraw) public withdrawalQueue;
    mapping(uint256 => QueuedDeposit) public depositQueue;
    mapping(uint256 => QueuedWithdraw) public withdrawalQueue;
    mapping(bytes32 => Collateral) public collaterals;
    mapping(uint256 => UserCollateral) public userCollaterals;
    mapping(uint256 => ShortPosition) public shortPositions;
    mapping(bytes32 => bool) public isPaused;
/// @dev bytes32(Key) => bool(status)
    mapping(bytes32 => bool) public isPaused;

[NC-16] Use SMTChecker

Description

The highest tier of smart contract behavior assurance is formal mathematical verification. All assertions that are made are guaranteed to be true across all inputs → The quality of your asserts is the quality of your verification.

Ref: https://twitter.com/0xOwenThurm/status/1614359896350425088?t=dbG9gHFigBX85Rv29lOjIQ&s=19

#0 - JustDravee

2023-03-26T23:04:11Z

Fine report

#1 - c4-judge

2023-05-03T03:28:03Z

JustDravee marked the issue as grade-b

#2 - JustDravee

2023-05-05T11:07:36Z

Compared this to other grade-Bs, and this is above in terms of quantity and quality. The signals aren't unique, but this is a good report. Will upgrade to grade-A

#3 - c4-judge

2023-05-05T11:07:41Z

JustDravee marked the issue as grade-a

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter