Nayms

QuantstampFiles named in the report

Quantstamp reviewed Nayms 2024 (Retainer) (Nayms) on 2 Dec 2022: 30 findings, 0 critical; 25 marked fixed, 2 acknowledged, 3 open.

No open critical findings
Open the reportOpen Nayms 2024 (Retainer)’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings30

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High001
Medium003
Low3221
HighDraining Entity Asset by `payDividendFromEntity()`Fixed

The `TokenizedVaultFacet.payDividendFromEntity()` can be called by anyone. The `LibTokenizedVault._payDividend()` function called under the hood would have the `_from` input as the `entityId`. Inside the `_payDividend()` function, it calls `_internalTransfer(_from, dividendBankId, _dividendTokenId, _amount)` to collect funds from the `_from` input (in the case here, the `entity`) to the virtual `dividendBankId`. The users can call this function to transform the assets of their entity to become dividends. Since anyone can call this, all entity members will be incentivized to immediately move all assets as dividends. The situation worsens as the `entity.asset` is supposed to be the same as the `simplePolicy.asset`. The attacker can immediately transform the funds for the insurance into a dividend.

MediumOverflow Blocking Offer ExecutionFixed

The `LibMarket._matchToExistingOffers()` function can overflow on the following lines: ``` result.remainingSellAmount -= currentSellAmount result.remainingBuyAmount -= currentBuyAmount; ``` It can overflow because the calculation logic on L199-214 for the `currentSellAmount` and the `currentBuyAmount` only takes care of one `result.remainingSellAmount` side or the `result.remainingBuyAmount` side. The other side is a derived amount, which can be larger than what is left in the remaining. Once the function overflows, it will revert and block the offer execution, although the trade should be valid. From our analysis, overflow will only happen when the `buyExternalToken` is true. In the case of `buyExternalToken == false`, if the `currentSellAmount` is larger than the `result.remainingSellAmount`, it means it is trying to match a maker's offer that is worse than the taker's offer since the taker needs to sell more to get the same buying amount. However, in the opposite case, the overflow of the `remaingingBuyToken` means the maker's offer is better than the taker expects.

MediumPolicy Asset Can Be Different than the EntityFixed

While creating a new `simplePolicy`, the function `_createSimplePolicy()` does not check that the policy `SimplePolicy.asset` variable is the same as `Entity.assetId`. This means a policy can have a different asset than the entity. It is also possible to update an entity's asset to become different than the rest of its policies through the function `_updateEntity()`.

MediumSystem Admin Privileges Can Be LostFixed

The highest privileges on the system are owned by the users in the group `GROUP_SYSTEM_ADMINS`, which are the users with the role `ROLE_SYSTEM_ADMIN`. It is possible to revoke the role `ROLE_SYSTEM_ADMIN` from a user with the following methods: 1. `ACLFacet.assignRole()`: assign the admin a new role. 1. `ACLFacet.unassignRole()`: unassign the admin the role 1. `ACLFacet.updateRoleGroup()`: assign new groups to the `ROLE_SYSTEM_ADMIN`. However, only members of that group can call that function. Aside from that, additional roles can be added to the group `GROUP_SYSTEM_ADMINS` if a user with the role `ROLE_SYSTEM_ADMIN` calls the function `ACLFacet.updateRoleGroup()`. 1. `NaymsOwnershipFacet.transferOwnership()`: the contract owner can transfer ownership but cannot only revoke ownership. However, it can transfer ownership to the `0x0` address, effectively renouncing his contract ownership. As the current number of users in the system having the role `ROLE_SYSTEM_ADMIN` is not tracked, it is possible to remain without any user having system admin privileges.

Low`_getWithdrawableDividendAndDeductionMath()` Returning Unexpected `_dividendDeduction` AmountFixed

The `LibTokenizedVault._getWithdrawableDividendAndDeductionMath()` can return a wrong `_dividendDeduction` value that adds one unexpectedly. Instead of checking against `_withdrawableDividend * _supply` on L276, it is supposed to check against `holderDividend * _supply`. Otherwise, whenever the `_withdrawnSoFar` is not zero, the `_dividendDeduction` would always be plus one.

Low`ReentrancyGuard` Colliding with Storage SlotFixed

Some facets inherit the `ReentrancyGuard` contract copied from the OpenZeppelin under `src/utils/`. However, the `ReentrancyGuard` implementation does not integrate with the diamond storage. The `ReentrancyGuard` will mutate a `_status` variable on the storage. The design of the `LibAppStorage.diamondStorage()` function (in the `AppStorage.sol`) uses slot zero instead of a randomized location. The `ReentrancyGuard` will override the data of the `AppStorage`. The current contract will continue to work as expected because the first slot of the `AppStorage` is reserved for the `nonces` mapping. In EVM, the storage slot for mapping will not be used (see: [blog](https://medium.com/coinmonks/solidity-tutorial-all-about-mappings-29a12269ee14#:~:text=In%20Solidity%2C%20mappings%20values%20are,key%20is%20hold%20in%20storage.)). The EVM relies on the slot number rather than the value inside the slot to hash to store and get the mapping value. However, if the variable `uint256 totalSupply` is the first variable in the `AppStorage` struct, then it will be overridden by the `ReentrancyGuard` contract. We set the severity as low because there is an immediate impact with the current codebase. However, this pattern is risky and might bring more considerable risks in the future.

LowAssigning Duplicated StakeholdersFixed

The `LibEntities._createSimplePolicy()` function assigns the stakeholders their roles. However, the stakeholders can be duplicated with the input. Thus, the for-loop might unexpectedly override the later role.

LowFulfilled Order Injecting Into Sorted ListFixed

The logic handling dust order is inconsistent between the `_createOffer()`, `_takeOffer()`, and `_executeLimitOffer()` functions of the `LibMarket` contract. The dust condition in the `_createOffer()` function is `<= LibConstants.DUST`; but the condition in the `_takeOffer()` is `< LibConstants.DUST`. Also, in the `_executeLimitOffer()` function, the condition to inject into the sorted list is not an inverse of the dust condition used in the `_createOffer()` function. For instance, when the `remainingBuyAmount` and `remainingSellAmount` are non-zero and `<= LibConstants.DUST` (e.g., one) in the `_executeLimitOffer()` function, the offer will be flagged as `LibConstants.OFFER_STATE_FULFILLED` on `_createOffer()#L247`. In contrast, the `_executeLimitOffer()` will continue to inject the already fulfilled order into the sorted list.

LowIncorrect Checks in `_assertValidOffer()`Fixed

In the function `_assertValidOffer()`: * the first require statement checks that an entity identified by `_entityId` already exists, but the error message is `must belong to entity to make an offer`; * the local boolean variable `sellTokenIsEntity` gets the value `true` if the token ID matches an existing entity. But it does not check that the token ID matches the entity ID. The same issue is for the local boolean `buyTokenIsEntity`.

LowInvalid Policy ConfigurationsFixed

While creating an entity, the code requires one of two things (see: `LibEntity.validateEntity()`): * `_entity.simplePolicyEnabled` to be false * `_entity.maxCapacity` should be greater than 0 However, the function `_updateAllowSimplePolicy()` can then be used to enable the `simplePolicyEnabled` for an entity with a `maxCapacity` of zero. Meanwhile, it is unclear whether it is acceptable to flag it back from `true` to `false` when the `_entity.maxCapacity` is non-zero.

Show 10 more
LowMissing Interface Extension in ContractsFixed

It is generally recommended for Solidity contracts to inherit interfaces in order to define an abstract behavior for the extending contract. This helps to ensure that the contract implements the required functions and adheres to the specified behavior. Several interfaces are declared but not explicitly extended by the associated contract: * interface `INayms` and contract `Nayms`; * interface `IAdminFacet` and contract `AdminFacet`; * interface `IEntityFacet` and contract `EntityFacet`; * interface `IMarketFacet` and contract `MarketFacet`; * interface `ISimplePolicyFacet` and contract `SimplePolicyFacet`; * interface `ISystemFacet` and contract `SystemFacet`; * interface `ITokenizedVaultFacet` and contract `TokenizedVaultFacet`; * interface `IOTokenizedVaultIOFacet` and contract `TokenizedVaultIOFacet`; * interface `IUserFacet` and contract `UserFacet`;

LowNot Supporting EIP-165Fixed

The diamond contract `Nayms` never sets the `ds.supportedInterfaces`. The `DiamondLoupeFacet.supportsInterface()` function will always return `false`. The [diamond spec](https://eips.ethereum.org/EIPS/eip-2535) does not necessarily require implementing EIP-165. However, having a non-functioning `supportsInterface()` function is confusing.

LowOff-Chain Component Getting Non-Unique `guid`Fixed

The `TokenizedVaultFacet.payDividendFromEntity()` and `LibTokenizedVault._payDividend()` functions get a `guid` input and emit an event with the `guid` data. The `guid` is for the off-chain component. However, since the contract never uses the value aside from emitting it in the event, the `guid` can be non-unique and goes against the assumption of a "Globally unique identifier of a dividend distribution", as stated in the code document of the `TokenizedVaultFacet.payDividendFromEntity()` function. Note that the `TokenizedVaultFacet.payDividendFromEntity()` function does not have authorization. So anyone can trigger the `guid` collision.

LowProduction Readiness ConcernFixed

A few Todos or commented-out codes exist in the codebase, which leads to the concern about whether the codebase is production ready: 1. `LibEntity._validateSimplePolicyCreation()`: 1. a todo on L42 `todo: ensure that the capital raised is >= max capacity`. 2. Commented out code on L45: `require(entity.collateralRatio > 0 && entity.maxCapacity > 0, "currency disabled")`. 1. `LibTokenizedVault._withdrawDividend()`: the commented-out code `// require(withdrawableDividend > 0, "_withdrawDividend: no dividend")` on L179. 1. `TokenizedVaultFacet.internalTransfer()`: the commented-out code `// require(LibTokenizedVault._internalBalanceOf(senderId, tokenId) >= amount, "internalTransfer: insufficient balance");` on L51.

LowRisk of `AppStorage` Mis-Ordering and Code Readability ConcernFixed

The contract `AppStorage` lists the main variables used by the protocol. However, the description of some variables can sometimes be ambiguous or incomplete. In particular: * the mechanism of dividends is explained in the middle of the contract; * the difference between the state variables `lpAddress` and `pool` is unclear; * the purpose of some variables is not described (`groupsConfig`, `canAssignConfig`); * the measuring unit of some variables is not described (`numOwnedTokens`, `equilibriumLevel`, `actualDiscount`, `maxDiscount`, `actualNaymsAllocation`, `targetNaymsAllocation`); * the fact that a given variable is currently used or not by the protocol is not always clear (`nonces`, commented variables related to `FEE BANK`); As a result, it can be challenging for a reader to obtain further details about a given variable. Also, the pattern of having commented storage variables in the middle of a proxy contract is dangerous. If the storage gets uncommented during the upgrade, it will break the storage ordering and likely break the whole system.

LowRisk of Calling `initialize()` TwiceFixed

The `InitDiamond.initialize()` might be triggered multiple times that can potentially reset the roles and configurations. If the contract owner calls the `diamondCut()` twice with the same `_init` and `_data` parameters, there is a risk that the system will accidentally be overridden.

LowRisk of Insolvent Balance with Non-Standard Erc20 Token IntegrationFixed

There are 2 concerns with the use of non-standard ERC20 tokens: 1. The external ERC20 token may use a non-standard decimal. For example, if token `A` uses a decimal value of `40`, then making a buy order with 1 token `A` will not even be possible since `10^40 > 2^128`, and `_assertAmounts()` make sure the amount is `<= type(uint128).max`. 1. The `LibTokenizedVaultIO._externalDeposit()` function relies on the input `_amount` to transfer, lock, and mint the internal tokens. However, the mechanism would fail if the ERC20 tokens take a fee or if the token is a rebaseable token. Integrating non-standard ERC20 tokens can have severe consequences, such as insolvent contracts, as it mints more than what is collected. Note that the risk is primarily reduced as only approved tokens can be used in the Nayms system. Only the system admin can call the `AdminFacet.addSupportedExternalToken()` function to add tokens.

LowRisk of ReentrancyFixed

The `TokenizedVaultIOFacet` contract interacts with external contracts (ERC20 tokens) without the `nonReentrant` guard. There are two main functions: 1. `TokenizedVaultIOFacet.externalDeposit()`: this function calls `LibTokenizedVaultIO._externalDeposit()`. However, the `LibTokenizedVaultIO._externalDeposit()` does not follow the [checks-effects-interactions pattern](https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html). In other words, there is no protection against reentrancy in this function. 2. `TokenizedVaultIOFacet.externalWithdrawFromEntity()`: the library function `LibTokenizedVaultIO._externalWithdraw` follows the checks-effects-interactions pattern, so the reentrancy risk is mitigated here. We did not find an obvious exploit path despite not having protection for the `TokenizedVaultIOFacet.externalDeposit()`. However, we recommend to add protections to avoid unexpected attack and mitigate risk for future code changes.

LowSignature Replay AttackFixed

The `LibEntities._createSimplePolicy()` function recovers the ECDSA signatures with the message exposed to the risk of a replay attack. The message signs the `ECDSA.toEthSignedMessageHash(_policyId)`. The only variable is the `_policyId`. The signature can be replayed if the same `_policyId` is reused in multiple deployments or chains. Also, the system admin can maliciously collect the signatures but then use them to attach with different `entityId` or `role` as that information is not part of the signature.

LowToken Transfer Restrictions in `InternalTransfer()` May Be Bypassed in `_InternalTransfer()`Fixed

In the function `internalTransfer()`, a require statement makes sure that the ID of the token transferred is not `LibConstants.STM_IDENTIFIER`. If the test passes, the internal function `_internalTransfer()` is called. However, there is no token restriction check in that second function, which can also be called from other functions. As a result, it may be possible to transfer tokens `LibConstants.STM_IDENTIFIER` via other paths. The severity level is "Undetermined" because the expected behavior is not documented.

Show 5 more
LowUnclear Spec or Mimatched DocumentationFixed

We noticed some places where the spec is unclear or the code mismatches with the documentation: 1. `LibACL._canAssign()`: the function plays an essential role in the library `LibACL`. However, it is hard to understand what should be its exact behavior due to incomplete documentation. For instance, the parameters need to be described. Also, it needs to be clarified why the function `_isParentInGroup()` is used to check if the `assignerId` has a role in the context of the system. 1. `LibObject._createObject()`: the code only checks if the object already exists, despite a comment indicating that it should check for associated parent accounts (`// check if the id has been used (has a parent account associated with it) and revert if it has`).

LowUnlocked PragmaFixed

Every Solidity file specifies in the header a version number of the format `pragma solidity >=0.8.13;`. The version statement implies an unlocked pragma, meaning that the compiler will use the specified version _and above_, hence "unlocked".

LowUnused Functions Lacking ValidationsFixed

We recommend adding validations to the unused functions that update storage variables. These functions may be intended for future features, but their inputs are currently not validated. It is unclear what kind of validations should be in place as it depends on the business requirements. Future code changes and audits should consider this issue and ensure appropriate validations are in place. 1. `LibAdmin._setPoolFee()` 1. `LibAdmin._setCoefficient()` 1. `LibAdmin._setDiscountToken()` 1. `LibAdmin._setTargetNaymsAllocation()` 1. `LibAdmin._setMaxDiscount()` 1. `LibAdmin._setEquilibriumLevel()`

LowUpdating an Entity Might Cause InconsistenciesFixed

The function `_updateEntity()` allows for updating the entity info, including the `collateralRatio`, which means that the new capital needed to underwrite policies could change. It is unclear to us whether this will have an impact on the protocol or not.

LowWrong Values in Emitted EventFixed

In `_payPremiumCommissions()`, the code emits the event `PremiumCommissionsPaid`, but the amount passed to the event is the total premium paid, not the premium commission paid.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (48)

  • diamonds/nayms/interfaces/IUserFacet.sol
  • diamonds/nayms/libs/LibAdmin.sol
  • diamonds/nayms/AppStorage.sol
  • diamonds/nayms/interfaces/IMarketFacet.sol
  • diamonds/nayms/interfaces/FreeStructs.sol
  • diamonds/nayms/libs/LibMarket.sol
  • diamonds/nayms/facets/ACLFacet.sol
  • diamonds/nayms/facets/SystemFacet.sol
  • diamonds/nayms/interfaces/IEntityFacet.sol
  • diamonds/nayms/facets/AdminFacet.sol
  • diamonds/nayms/interfaces/IAdminFacet.sol
  • diamonds/nayms/InitDiamond.sol
  • diamonds/nayms/facets/UserFacet.sol
  • diamonds/nayms/libs/LibACL.sol
  • diamonds/nayms/facets/SimplePolicyFacet.sol
  • diamonds/shared/interfaces/IDiamondCut.sol
  • diamonds/shared/interfaces/IERC173.sol
  • diamonds/nayms/facets/TokenizedVaultIOFacet.sol
  • diamonds/nayms/libs/LibSimplePolicy.sol
  • diamonds/nayms/facets/MarketFacet.sol
  • diamonds/nayms/interfaces/IACLFacet.sol
  • diamonds/nayms/libs/LibTokenizedVaultIO.sol
  • diamonds/nayms/libs/LibTokenizedVault.sol
  • diamonds/nayms/interfaces/ISimplePolicyFacet.sol
  • diamonds/shared/facets/DiamondLoupeFacet.sol
  • diamonds/shared/interfaces/IDiamondLoupe.sol
  • diamonds/nayms/Modifiers.sol
  • diamonds/nayms/libs/LibNaymsToken.sol
  • diamonds/shared/facets/NaymsOwnershipFacet.sol
  • diamonds/shared/interfaces/IERC165.sol
  • diamonds/shared/libs/LibDiamond.sol
  • diamonds/nayms/INayms.sol
  • diamonds/shared/libs/LibMeta.sol
  • diamonds/nayms/libs/LibFeeRouter.sol
  • diamonds/nayms/libs/LibHelpers.sol
  • diamonds/nayms/facets/TokenizedVaultFacet.sol
  • diamonds/nayms/facets/EntityFacet.sol
  • diamonds/nayms/libs/LibConstants.sol
  • diamonds/nayms/facets/NaymsTokenFacet.sol
  • diamonds/shared/facets/DiamondCutFacet.sol
  • diamonds/nayms/interfaces/ITokenizedVaultFacet.sol
  • diamonds/nayms/interfaces/ITokenizedVaultIOFacet.sol
  • diamonds/shared/facets/OwnershipFacet.sol
  • diamonds/nayms/interfaces/ISystemFacet.sol
  • diamonds/nayms/Nayms.sol
  • diamonds/nayms/interfaces/INaymsTokenFacet.sol
  • diamonds/nayms/libs/LibEntity.sol
  • diamonds/nayms/libs/LibObject.sol

Part of Nayms 2024 (Retainer)’s security record

Nayms 2024 (Retainer)
Audits3 audits by 1 firmLast audit May 2024, by Quantstamp.
Critical findingsNo open critical findingsAcross 3 reports.
Open Nayms 2024 (Retainer)’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us