Nayms 2024 (Retainer) security record

Base, Ethereumnayms.comXGitHub

As of 3 Nov 2026, Nayms 2024 (Retainer) has 3 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 3 May 2024. Those reports list no open critical findings.

Critical findings
No open critical findings
Across 3 reports.
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.

Audits3

Newest first

AuditSecurity firmDateScopeCritical findingsReport
Nayms 2024 (Retainer)Security firm: QuantstampDate: Scope: Repository at commit 2f08d24No open critical findingsOpen · certificate.quantstamp.com
Nayms 2Security firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com
NaymsSecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com

Findings93

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

SeverityOpenAcknowledgedFixed
Critical000
High008
Medium229
Low72738
HighA malicious user can reduce other users' rewards due to incorrect accounting of stake balancesNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

In the function `_unstake()`, the stake balance of the entity is not reduced for the current interval, namely `s.stakeBalance[vTokenId][_entityId]` is not reduced by the amount unstaked. This is problematic because a user can stake and unstake in the current interval, but this only increases the stake balance of the entity for the current interval. This means the actual amount of tokens staked in `_entityId` is not accurately represented by the `stakeBalance` mapping. This in turn leads to wrong accounting in `_payRewards()` if the entity admin decides to pay rewards at the current interval after the user's unstaking. In particular, some rewards will be unclaimable because a portion of the rewards is allocated to the "unstaked" user. Furthermore, an attacker can deny other users from claiming their rewards by staking a huge amount and then immediately unstaking.

Open the report
HighIncorrect accounting when users of a staking entity stake into that entityNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

As staking happens on behalf of a users' entity and due to missing checks, it is possible for users of a staking entity to stake into their entity on its behalf. In particular, this could lead to following accounting issues: 1. `LibTokenizedVaultStaking._stake()`: Double accounting in `L124`, `L129` and `L142-L147`. 2. `AppStorage.stakeCollected[][]`: Variable is used to track a staker's last collected interval as well as a staking entity's last paid out reward interval, leading to incorrect last interval accounting for users of a staking entity.

Open the report
HighNot All Objects in the System Are Existing Objects or Have an Object Type, Which Could Lead to Unexpected Collisions or Issues for Future FeaturesNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

The Nayms platform uses a `bytes32` identifier to distinguish the different objects in the system and store relational attributes between them in mappings, for instance, the token balances of each object, or the access permissions between objects. The mapping `existingObjects(bytes32 => bool)` stores the fact that an object with a specific identifier exists or not. However, not all objects identified by an identifier are in this mapping. For instance, roles and groups have an identifier but are not considered as existing in the system. This could lead to collisions when we can create an object `b` with the same identifier as the object `a` which is already used by the system but is not an existing object according to the mapping. One example of a negative consequence is illustrated in the Exploit section below. Another example is the fact that, when the first address is granted a `sysAdmin` role in `LibInitDiamond.setSystemAdmin()`, the associated identifier of this address becomes an existing id (mapping `existingObjects`). However, when other `sysAdmin` roles are granted via `LibACL._assignRole()`, the addresses receiving that role do not become existing objects. Finally, the concept of object type was introduced in the last update of the codebase and adds to the identifier of an object `12` bytes to define the type of the object. However, objects that were created before this update still exist in the system but do not have in their identifier these `12` bytes to define their type. As a result, if in the future, some features check that a given object must be of a given type (for example: a policy), legacy objects created before the update will not be compatible with this new feature.

Open the report
HighUser can artificially inflate stake boosts for the next interval by repeating stake and unstake actionsNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

In `_unstake()`, we only set `s.stakeBoost[vTokenId][_stakerId] = 0;` back to zero, but we do not set the stake boost for the next `vTokenId` back to zero. When a user stake in the current interval, part of the stake boost will be allocated to the next `vTokenId` proportional to the time it was staked between the current interval and the next interval, as shown in the following lines: ``` uint256 boostTotal = (_amount * _getA(_entityId)) / _getD(_entityId); uint256 boostNext; if (_isStakingInitialized(_entityId)) { boostNext = (boostTotal * (block.timestamp - _calculateStartTimeOfCurrentInterval(_entityId))) / s.stakingConfigs[_entityId].interval; } uint256 boost = boostTotal - boostNext; // give to the staker s.stakeBoost[vTokenId][_stakerId] += boost; s.stakeBoost[nextVTokenId][_stakerId] += boostNext; ``` Since the stake boost is never reset for the next `vTokenId`, an attacker can inflate the stake boost for the next interval by repeatedly staking and unstaking in the current interval. Then, at the start of the next interval, an attacker can simply stake a small amount, but he will be able to claim more rewards than should be allowed due to the inflated boost.

Open the report
HighArbitrary Actions Can Be Executed when Upgrading the Diamond PatternNayms 2 · Quantstamp · 5 May 2023Fixed

In the standard Diamond Pattern, upgrades work as follows: facets (a contract implementing a particular behavior) can be added, replaced, or removed using the function: ``` { DiamondCutFacet.diamondCut( FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata ) } ``` </br>The second and third parameters are used (if both have a non-zero value) to execute a `delegateCall()` from the current contract to any arbitrary contract deployed at address `_init`, to a specific function and with specific parameters defined in `_calldata`. The current two-phase diamond facet cut process implemented in `PhasedDiamondCutFacet.diamondCut()` makes sure for security purposes that the following actions associated with an upgrade of the diamond are executed by two distinct addresses: * allow in advance in `GovernanceFacet.sol` an upgrade to be executed (action only doable by a `sysAdmin`); * execute an allowed upgrade (action only doable by the owner of the diamond contract); </br>An upgrade is allowed only if its hash (which is the hash of the parameter `_diamondCut`) can be found in the storage mapping `upgradeScheduled`, the associated value being the expiration timestamp of that upgrade authorization. However, that authorization in advance lets the contract owner use any values for the parameters `_init` and `_calldata`. As a result, it becomes possible to override any value anywhere in the storage of the Diamond. The owner just has to deploy an arbitrary contract at the address `_init` and call a function that overrides memory slots. For example, he could: 1. drain all ERC20 funds deposited to the system; 2. update the sysAdmins of Nayms; 3. modify balances of the internal ledger; </br>Note that this list is non-exhaustive. Also, the contract owner is considered a trusted user.

Open the report
HighDouble-Counting of Dividend Payout Results in Capital Providers Not Being Able to Withdraw DividendsNayms 2 · Quantstamp · 5 May 2023Fixed

The Nayms system currently restricts the number of `dividendDenomiation` to be at most `maxDividendDenominations`, which is currently set as `1`. However, in `LibTokenizedVault`, this restriction is checked before the value is updated, so the check can be bypassed. Specifically, we have the following snippet in `_payDividend()`: ```solidity! if (s.dividendDenominationIndex[_to][_dividendTokenId] == 0) { if (s.dividendDenominations[_to].length > LibAdmin._getMaxDividendDenominations()) { revert("exceeds max div denominations"); } s.dividendDenominationIndex[_to][_dividendTokenId] = uint8(s.dividendDenominations[_to].length); s.dividendDenominationAtIndex[_to][uint8(s.dividendDenominations[_to].length)] = _dividendTokenId; s.dividendDenominations[_to].push(_dividendTokenId); } ``` This means we can add more than `1` dividend token id in the list `dividendDenominations`. However, this has a harmful unintended side effect, where a same `dividendTokenId` can be added to the array. Suppose we called `_payDividend(_to = Alice, _dividendTokenId = WETH)` for the first time. Note that after the function call, `s.dividendDenominationIndex[Alice][WETH]` will still be `0` because `uint8(s.dividendDenominations[Alice].length) = 0`, and now `s.dividendDenominations[Alice] = [WETH]`. Now, if we call `_payDividend(_to = Alice, _dividendTokenId = WETH)` again, it will pass both checks in `_payDividend`, and append `WETH` to the array `s.dividendDenominations[Alice]` again. This leads to a double-counting issue in `_normalizeDividends` as `s.withdrawnDividendPerOwner[_tokenId][dividendDenominationId][_to] += dividendDeductionIssued;` will be executed twice in the loop, adding the same value twice to the same `dividendDenominationId`. Furthermore, since this value is used to calculate the dividend payout for an entity, the entity may receive a payout lower than expected because the system mistakenly assumes that the entity withdrew more dividends than it actually had. To illustrate the above issue, consider the following example: 1. Alice starts with `500 WETH` in its internal balance; 2. Alice calls `startTokenSale(ALICE, 100, 100 WETH)`, selling `100 ALICE` tokens for `100 WETH`; 3. Now Alice owns `100 ALICE` but they are locked; 4. Alice pays `100 WETH` as a dividend; 5. Alice pays `100 WETH` as a dividend; 6. Bob buys all `100 ALICE` from Alice. Here, during the transfer, Alice would have withdrawn the `200 WETH` dividend owed to her, so her balance is `600 WETH` (`300` + `200` for dividend + `100` from Bob's purchase); 7. Bob transfers all `100 ALICE` back to Alice; 8. Alice pays `500 WETH` as a dividend; 9. Alice tries to withdraw the `500 WETH` dividend but can only withdraw `100 WETH` due to the double counting issue and the other issue described in this report about Entity Token Holders not receiving future dividends;

Open the report
HighEntity Token Holders May Not Receive Future DividendsNayms 2 · Quantstamp · 5 May 2023Fixed

There are currently some miscalculations related to dividend payout accounting. In particular, capital providers might not be able to get future dividends if they trade entity tokens with each other on the marketplace. The cause is in the function `LibTokenizedVault._internalTrasfer(_from, _to, ...)` where the values in the mapping `s.withdrawnDividendPerOwner` only ever increase. However, this means `s.withdrawnDividendPerOwner` will be increased for the previous owner even when they no longer own the entity token, as the tokens now belong to the new owner. Furthermore, following the comments in `DIVIDEND PAYOUT LOGIC` in `LibTokenizedVault`, `totalWithdrawnDividendPerOwner` is supposed to be reduced for the previous owner, but this is not the case in the actual implementation.

Open the report
HighDraining Entity Asset by `payDividendFromEntity()`Nayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
Medium`ACLFacet.updateRoleGroup()` may change `GROUP_SYSTEM_ADMINS`, bypassing the existing checkNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

The function `ACLFacet.updateRoleGroup()` allows system administrators to change the group membership for a given role. While the following check prevents changing the membership to group `LC.GROUP_SYSTEM_ADMINS`, it can be bypassed: ``` require(!LibString.eq(_group, LC.GROUP_SYSTEM_ADMINS), "system admins group is not modifiable"); ``` In details, this check can be bypassed by providing a `_group` string with the value `LC.GROUP_SYSTEM_ADMINS` padded with enough zero bytes such that the function `eq()` returns `false` but the function `LibACL._updateRoleGroup()` still uses the value of `LC.GROUP_SYSTEM_ADMINS` (i.e. `System Admins\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000` - The string `System Admins` padded with null-bytes to 32 bytes length, which may be passed like this or may contain arbitrary other bytes afterwards).

Open the report
MediumUnrestricted Access Control Rule for Internal Transfers From Objects' ChildrenNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

The function `TokenizedVaultFacet.internalTransferFromEntity()` is no longer protected by an "onlyEntityAdmin" modifier. Since the fact that the parent of `msg.sender` is not checked to be an entity, any address being the child of an object id owning tokens can trigger a transfer of any token to an arbitrary other internal address. This could lead to unexpected consequences in the current version of the system and future ones as well.

Open the report
Show 10 more
MediumId Aliasing Between Addresses and Associated ObjectidNayms 2 · Quantstamp · 5 May 2023Fixed

The function `LibHelpers._getAddressFromId(bytes32 id)` downcasts 32 bytes to an address, discarding the lower 12 bytes. In other terms, it will return the same address for different ids: * `id_a = 0x0102030405060708091011121314151617181920000000000000000000000000`; * `id_b = 0x0102030405060708091011121314151617181920FFFFFFFFFFFFFFFFFFFFFFFF`; </br>That function is used by several functions: * the function `UserFacet.getAddressFromExternalTokenId()` returns the same address for `id_a` and `id_b`; * it could lead to limit offer aliasing in the function `LibMarket._executeLimitOffer()`, in the special case where the parameters `_sellToken` or `_buyToken` represent one of two existing entities having the same 20 upper bytes. * the function `LibAdmin._isSupportedExternalToken()` returns true for `id_a` and `id_b`, even if only `id_a` represents an existing entity. As a result: * `AdminFacet.isSupportedExternalToken()` returns true for `id_a` and `id_b`; * simple policies and entities can be created with different asset ids such that `id != LibHelpers._getIdForAddress(LibHelpers._getAddressFromId(id))`. That could result later in unexpected behavior. * `LibTokenizedVault._payDividend()` could have unexpected behavior; </br>External endpoints that could be impacted are: * `MarketFacet.executeLimitOffer()` - executable by any user from an entity; * `TokenizedVaultFacet.payDividendFromEntity()` - executable only by the admin of an entity; * `EntityFacet.startTokenSale()` - executable only by sysMgr;

Open the report
MediumIncorrect Accounting of `sysAdmins` when Reassigning an Existing Admin RoleNayms 2 · Quantstamp · 5 May 2023Fixed

The function `LibACL._assignRole()` may grant an object ID the `ROLE_SYSTEM_ADMIN`, in which case the state variable `sysAdmins` is increased. While there exists the `LibACL._unassignRole()` function which revoke from a given object ID the admin role and correspondingly decreases the `sysAdmins` counter variable, it is possible to reassign an existing admin to another role using `LibACL._assignRole()`, which however will not decrease the `sysAdmins` counter, leading to incorrect accounting of the number of currently active `ROLE_SYSTEM_ADMIN` roles. The concrete impact would be that the check in `LibACL.sol#L60`, which checks that at least one `ROLE_SYSTEM_ADMIN` role remains in the system when unassigning a role, could falsely assume one or more `ROLE_SYSTEM_ADMIN` roles remain in the system, while in fact this is no longer the case and thereby leave the protocol in an inoperable state.

Open the report
MediumInvalid Entity UpdateNayms 2 · Quantstamp · 5 May 2023Fixed

The lack of crucial checks when updating an Entity allows for an invalid state to persist on an Entity, ultimately denying the ability of this Entity to write new policies. In the function `LibEntity._updateEntity()`, `validateEntity()` is called to check that the `utilizedCapacity` of the new Entity does not exceed the `maxCapacity` for that Entity. In the case where there is a new `collateralRatio` being assigned to the Entity, there is a `newUtilizedCapacity` calculated for the Entity. This `newUtilizedCapacity` is not checked to ensure that it does not exceed the specified `maxCapacity`. As a result, an Entity can be updated to have a `utilizedCapacity` that exceeds the `maxCapacity`, which would deny the ability of this Entity to pass the checks required to write new policies. Currently, the balance of the Entity and the updated values are only checked if there is an update to the `collateralRatio` of the Entity. In the case where the `collateralRatio` of the Entity is not being updated, you can have an updated `utilizedCapacity` that exceeds the internal balance of that Entity. More generally, it appears that the `utilizedCapacity` of an Entity can be updated, even if the `collateralRatio` has not been updated. Logically, the utilized capacity of an Entity should only be updated if the collateral ratio is changed.

Open the report
MediumMore than the Premium Amount Can Be Spent when a Premium Is PaidNayms 2 · Quantstamp · 5 May 2023Fixed

The payment of a premium can be done with the function `LibFeeRouter._payPremiumCommissions()`. Premium commissions can be defined at 2 levels: 1. at the level of the policy: a list of `commissionReceivers` is defined, each having a defined `commissionBasisPoints` fee assigned; 2. at the level of the system: three actors (NaymsLtd, NDF, STM) have dedicated `commissionBasisPoints` fees assigned: `premiumCommissionNaymsLtdBP`, `premiumCommissionNDFBP`, `premiumCommissionSTMBP`; </br>For the policy level, the function `LibEntity._validateSimplePolicyCreation()` makes sure that the sum of all commissionBasisPoints is `< 10000`. For the system level, the function `InitDiamond.initialize()` is used to initially define the three variables. Also, the function `LibFeeRouter._updatePolicyCommissionsBasisPoints()` is used to update the three variables and make sure that the sum is `< 10000`. However, none of the operations to create a policy and update system-level premium commissions makes sure that there is no policy in the system for which the sum of policy-level and system-level `commissionBasisPoints` is `> 10000`. In that situation, more than the premium amount to be paid could be spent by the entity. Alternatively, the payment could also fail if there is no sufficient amount to be spent. The likelihood is low, as only `sysAdmin` and `sysManager` can execute these functions. Also, in the same function, the event `PremiumCommissionsPaid` only records the sum of system-level commissions that have been paid. It does not record what has been paid for the policy-level commissions.

Open the report
MediumOverflow Blocking Offer ExecutionNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
MediumPolicy Asset Can Be Different than the EntityNayms · Quantstamp · 2 Dec 2022Fixed

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()`.

Open the report
MediumSystem Admin Privileges Can Be LostNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
Low[PR-100] Incorrect accounting of `depositTotal`Nayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

In `LibTokenizedVaultIO`, functions `_externalWithdraw()` and `_externalDeposit()`, the value of `depositTotal` should be updated with the exact amounts deposited and withdrawn instead of the requested amounts (cases of rebasing tokens and fee-on-transfer tokens).

Open the report
Low[PR-100] Unclear rationale for the update of `depositTotal` with an hardcoded amount instead of the current balanceNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

In `LibTokenizedVault`, function `_rebaseERC20()`, the rationale of providing an `_amount` parameter to the function `_rebaseERC20()` is unclear, and could harm the system, since any value between `0` and `currentBalance` is a valid input to hardcode the new value of `s.depositTotal[_tokenId]`. An alternative could be to use all the amount available instead.

Open the report
LowChildren Objects Can Trigger Staking-Related Operations on Behalf of Their ParentNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

The normal behavior for the staking feature is to let children objects of an entity trigger the following operations on behalf of their parent entity: `stake()`, `unstake()`, and `collectRewards()`. While this behavior makes sense, the fact that the parent is an entity is not checked. As a result, edge cases may be possible for parent-child relationships where the child is an address (EOA or contract) able to call one of these three operations, and the parent is not an entity that owns the staking token. In such a case, the child will be able to trigger a transfer of staking tokens on behalf of his parent. Such an operation, if feasible, could negatively impact the system and its accounting.

Open the report
Show 10 more
LowConsolidation of best practicesNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

1. qs-badge-fixed In the repo `naym-coin`: some items are imported but not used. 2. qs-badge-fixed In `naym-coin/InitDiamond`: there is a "//todo" comment. 3. In `contracts-v3/ACLFacet`: consider caching in a local variable the value of `LibHelpers._stringToBytes32(_role)` to save gas. 4. qs-badge-fixed In `contracts-v3/LibSimplePolicy `: in `_payClaim()`, we could replace `>` with `>=` in the following statement: `require(availableBalance > _amount, "not enough balance")`. 5. In `contracts-v3/LibFeeRouter`: </br>a. In `_addFeeSchedule()`, consider caching in a local variable the value of `feeSchedule.receiver.length` to save gas. </br>b. Code could be reused between the functions `_payPremiumFees()`, `_payTradingFees()`, `_calculatePremiumFees()` and `_calculateTradingFees()` to reduce the code complexity. </br>c. qs-badge-fixed In `_calculatePremiumFees()`, the line `uint256 totalReceiverCount;` can be merged with the next one. </br>d. qs-badge-fixed The fields `FeeAllocation.from` and `FeeAllocation.token` are not used. </br>e. qs-badge-fixed In the function `_payPremiumFees()`, the fact that `fee > 0` is only checked in the second for loop. 6. qs-badge-fixed In `contracts-v3/SimplePolicyFacet`: in `getSimplePolicyInfo()`, consider caching in a local variable the value of `LibSimplePolicy._getSimplePolicyInfo(_policyId)` to save gas. 7. qs-badge-fixed In `contracts-v3/StakingFacet`: according to [Nayms code standard](https://docs.nayms.com/contracts-docs-copy/docs-static/code_standards/), `AppStorage` should not be accessed via facets. 8. qs-badge-fixed In `contracts-v3/LibTokenizedVaultIO`: double-check if the keyword `indexed` should be used for the events `ExternalDeposit` and `ExternalWithdraw`. 9. In `contracts-v3/LibMarket`: </br>a. qs-badge-fixed In `_createOffer()`, cache in a local variable the values of `s.objectMinimumSell[_buyToken]` and `s.objectMinimumSell[_sellToken]` to save gas. </br>b. qs-badge-fixed In `_takeOffer()`, cache in a local variable the values of `s.objectMinimumSell[s.offers[_offerId].sellToken]` to save gas. </br>c. Consider using a constant instead of a magic value `1`. 10. In `contracts-v3/LibTokenizedVaultStaking`: </br>a. qs-badge-fixed In `addUniqueValue()`, cache in a local variable the value of `rewards.currencies.length` to save gas. </br>b. qs-badge-fixed In `addUniqueValue()`, the line `rewards_.lastPaidInterval = i;` could be removed from the loop, and updated after the loop, based on the size of the list. </br>c. qs-badge-fixed Code could be reused between the functions `_getStakingStateWithRewardsBalances()` and `getStakingState()` to reduce the code complexity. 11. qs-badge-fixed In `contracts-v3/LibObject`: the function `LibObject._tokenSymbolNotUsed()` is not used. 12. qs-badge-fixed In `contracts-v3/Modifiers`: the modifier `assertIsInGroup`is not used anymore.

Open the report
LowEmergency locking mechanism could be improved to improve the exhaustiveness, robustness, and reactivity of the systemNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

The system provides the option to pause functions atomically, as well as one way to pause all the flows in a single transaction. It is said that all functions involving updates of funds should be lockable. Three functions with these properties are currently not lockable: * `SimplePolicyFacet.cancelSimplePolicy()`; * `EntityFacet.createSimplePolicy()`; * `SystemFacet.createEntity()`; Also, in case of an emergency, people in charge of pausing the system will need to take time to make sure about the impact of pausing a subset of functions atomically, because locking one function could make other functions active. In this situation, some users could take this opportunity to interact with the system because they are sure that other users with opposite interests cannot do operations (currently locked) in immediate reaction to the operation. Identifying sets of conflicting lockable functions would reduce the time to think about which set of functions we have to lock if we identify a potential hack on a given function.

Open the report
LowIncorrect values used for events `ExternalDeposit `, `OrderMatched ` and `TokenStakingStarted`Nayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

1. In the function `_externalDeposit()`, the amount to be used for the event `ExternalDeposit` should be `mintAmount` instead of `_amount`. 2. In the function `_matchToExistingOffers()`, the amount to be used for the events `OrderMatched` should be the final amounts calculated, meaning the normalized amounts if there was a normalization. 3. In the function `_initStaking()`, the event `TokenStakingStarted` is using `block.timestamp`, which is redundant since it is already metadata of the event. Also, it is unclear if the objective is to log the moment when the boosts will start or the moment when staking is initiated.

Open the report
LowKey State Changes Are Missing EventsNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

To validate the proper deployment and initialization of the contracts, it is a good practice to emit events. Also, any important state transitions can be logged, which is beneficial for monitoring the contract and tracking eventual bugs or hacks. Below is a list of events that could be added: 1. in the repo `naym-coin`, when the storage variables `sysAdmins` and `minter` are updated; 2. in `contracts-v3/LibAdmin._addSupportedExternalToken()`, when the initial amount of `minimumSell` is set; 3. `In contracts-v3/LibFeeRouter._removeFeeSchedule()`, when fee schedules are updated;

Open the report
LowMismatch between code and documentation in the function `internalTransferBySystemAdmin()`Nayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

In the function `internalTransferBySystemAdmin()`, the documentation mentions that: 1. it should let the admin transfer funds from one entity to another. However, no check makes sure that the sender and the receiver are entities. As a result, the admin can transfer tokens from any object to any object; 2. the caller must be an entity admin of the from entity, but this is not enforced;

Open the report
LowPotential incorrect checksNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

1. In the function `_validateSimplePolicyCreation()`, the line `require(commissionReceiversArrayLength <= _stakeholders.roles.length, "too many commission receivers"); // error too many POLICY level commission receivers` may be updated to enforce strict equality. 2. In the function `_removeFeeSchedule()`, the line `require(_entityId != LibConstants.DEFAULT_FEE_SCHEDULE, "cannot remove default fees");` seems incorrect since we compare an entity ID with a `feeScheduleType`. It should be confirmed to be working as expected. 3. In the original code, there was a rule enforced to make sure that an internal trade is either exchanging external supported tokens for p-tokens or vice-versa. This rule was removed in the updated codebase. The intention of the conditional blocks `if (buyExternalToken) ... else ...`) was that if we are not buying external tokens (i.e. `buyExternalToken` is `false`), then the maker must be buying external token. However, this conditional block is not suited anymore for the case where we are trading external token for external token.

Open the report
LowPotential Silent Failures During Signature VerificationNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

The function `getSigner()` is used to get the signer of a signature using the `ECDSA` library. This function can silently fail and return an incorrect signature: 1. if `signature.length != 65`, `tryRecover()` will be called with a value of `0` for `r`, `s` and `v`; 2. if `tryRecover()` returns an error message, the function will not revert. Note that the signer can be `address(0)` but also a random address. The severity is [Low], because the signature recovery feature is only used when creating a policy and an additional check makes sure that the `address(0)` is not in the list of stakeholders. Still, it could be technically possible to add `address(0)` as a stakeholder by the system manager, but it is unlikely.

Open the report
LowTransfer of ownership can be lost if transferred to an address owned by no oneNayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

In the contract `NaymsOwnershipFacet`, in the function `transferOwnership()`, the ownership can be lost if `_newOwner` is an address owned by no one. A two-step transfer could solve this.

Open the report
LowUnsafe downcast in `_calculateStartTimeOfInterval()`Nayms 2024 (Retainer) · Quantstamp · 3 May 2024Fixed

In the function `_calculateStartTimeOfInterval()`, an unlikely unsafe downcast can happen when the value of `intervalTime_` is downcasted to `uint64`.

Open the report
LowAdding Wrapped Participation Tokens as Supported External Tokens May Lead to Incorrect AccountingNayms 2 · Quantstamp · 5 May 2023Fixed

While the function `LibAdmin._addSupportedExternalToken()` does check whether or not the to-be-checked address `_tokenAddress` is an already supported external token, it does not check whether or not it refers to a wrapped participation token (see contract `./src/erc20/ERC20Wrapper.sol` - Note: that contract was out-of-scope for this audit). Adding such a token could lead to incorrect accounting of the underlying balances.

Open the report
Show 10 more
LowApplication Monitoring Can Be Improved by Emitting More EventsNayms 2 · Quantstamp · 5 May 2023Fixed

In order to validate the proper deployment and initialization of the contracts, it is a good practice to emit events. Also, any important state transition can be logged, which is beneficial for monitoring the contract, and also tracking eventual bugs, or hacks. Below we present a non-exhaustive list of events that could be emitted to improve the application management: 1. `LibObject._createObject()` (all 3 functions impacted): Changes made to `LibAppStorage.diamondStorage().existingObjects[]`, `LibAppStorage.diamondStorage().objectParent[]` and `LibAppStorage.diamondStorage().objectDataHashes[]`. 2. `LibObject._setDataHash()`: Changes made to `LibAppStorage.diamondStorage().objectDataHashes[]`. 3. `LibObject._setParent()`: Changes made to `LibAppStorage.diamondStorage().objectParent[]`. 4. `LibObject._enableObjectTokenization()`: Changes made to `LibAppStorage.diamondStorage().objectTokenSymbol[]` and `LibAppStorage.diamondStorage().objectTokenName[]`. 5. `LibAdmin._lockFunction()`: Changes made to `LibAppStorage.diamondStorage().locked[]`. 6. `LibAdmin._unlockFunction()`: Changes made to `LibAppStorage.diamondStorage().locked[]`. 7. `LibFeeRouter._payPremiumCommissions()`: The emitted event `PremiumCommissionsPaid()` does not take into account the policy-specific commissions (see lines `L21-L25`). 8. `LibFeeRouter._updateTradingCommissionsBasisPoints()`: Changes made to `LibAppStorage.diamondStorage().tradingCommission*` state variables. 9. `LibFeeRouter._updatePolicyCommissionsBasisPoints()`: Changes made to `LibAppStorage.diamondStorage().premiumCommission*` state variables.

Open the report
LowContract Owner Can Become System AdminNayms 2 · Quantstamp · 5 May 2023Fixed

The two-phase upgrade mechanism of the diamond contract is a way to prevent any upgrade can be executed by a single address, meaning the `newOwner` having also a `sysAdmin` role. Checks make sure in `NaymsOwnershipFacet.transferOwnership()` that is not possible to set a new contract owner if that owner also has a `sysAdmin` role. </br>However, the opposite can technically be done, meaning: 1. set a new contract owner using `NaymsOwnershipFacet.transferOwnership()`, only doable by a `sysAdmin` (due to the modifier `assertSysAdmin`); 2. executes `ACLFacet.assignRole()` to assign the role `sysAdmin` to the new contract owner; However, only a `sysAdmin` can perform that action.

Open the report
LowInsufficient Contract PausabilityNayms 2 · Quantstamp · 5 May 2023Fixed

In case of a hack or other security-related incident, it is recommended to have sufficient pausability of critical contract functionality related to the transfer of funds. This can help by giving the developers more time for identifying the issue and eventually mitigate it, before more harm is done and then resume said functions again. In this regard, only the function `TokenizedVaultIOFacet.externalWithdrawFromEntity()` may be paused, still leaving funds at risk in case of an incident.

Open the report
LowIssues Identified with FuzzingNayms 2 · Quantstamp · 5 May 2023Fixed

Using tools for fuzzing, three issues were identified: 1. the function `_addressToBytes32()` is a duplicate of the function `_getIdForAddress()`; 2. the function `_stringToBytes32()`, the fact that the length of `strIn` must be `<= 32` is not checked; 3. the function `_bytes32ToString()` always performs NULL-padding up to 32 bytes, leading to different strings.

Open the report
Low`_getWithdrawableDividendAndDeductionMath()` Returning Unexpected `_dividendDeduction` AmountNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
Low`ReentrancyGuard` Colliding with Storage SlotNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowAssigning Duplicated StakeholdersNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowFulfilled Order Injecting Into Sorted ListNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowIncorrect Checks in `_assertValidOffer()`Nayms · Quantstamp · 2 Dec 2022Fixed

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`.

Open the report
LowInvalid Policy ConfigurationsNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
Show 10 more
LowMissing Interface Extension in ContractsNayms · Quantstamp · 2 Dec 2022Fixed

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`;

Open the report
LowNot Supporting EIP-165Nayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowOff-Chain Component Getting Non-Unique `guid`Nayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowProduction Readiness ConcernNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowRisk of `AppStorage` Mis-Ordering and Code Readability ConcernNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowRisk of Calling `initialize()` TwiceNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowRisk of Insolvent Balance with Non-Standard Erc20 Token IntegrationNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowRisk of ReentrancyNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowSignature Replay AttackNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowToken Transfer Restrictions in `InternalTransfer()` May Be Bypassed in `_InternalTransfer()`Nayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
Show 5 more
LowUnclear Spec or Mimatched DocumentationNayms · Quantstamp · 2 Dec 2022Fixed

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`).

Open the report
LowUnlocked PragmaNayms · Quantstamp · 2 Dec 2022Fixed

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".

Open the report
LowUnused Functions Lacking ValidationsNayms · Quantstamp · 2 Dec 2022Fixed

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()`

Open the report
LowUpdating an Entity Might Cause InconsistenciesNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report
LowWrong Values in Emitted EventNayms · Quantstamp · 2 Dec 2022Fixed

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.

Open the report

Contracts

No deployed address is recorded for these audits.

The reports name a repository or files. Open a report to see its scope.

The repositories and files the reports name

Show this on your site

Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.

Content

Facts

Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-tb-project-slug="nayms-2024-retainer"
></div>
</body>

Paste the code where the label should appear. It uses no API credit and updates by itself.

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