neutrl-contracts
Cantina Security Report
Organization
- @Neutrl
Engagement Type
Spearbit Web3
Period
-
Researchers
Findings
Medium Risk
1 findings
1 fixed
0 acknowledged
Low Risk
6 findings
5 fixed
1 acknowledged
Informational
17 findings
13 fixed
4 acknowledged
Gas Optimizations
6 findings
4 fixed
2 acknowledged
Medium Risk1 finding
Users can decrease their asset lock end times
State
- Fixed
PR #35
Severity
- Severity: Medium
≈
Likelihood: High×
Impact: Low Submitted by
Kurt Barry
Description
The
AssetLock.lockAsset()function allows users to add assets to an existing locked position. They are also able to specify a new duration which is applied to the position without regard for the existing expiration:lockEndTime: uint64(block.timestamp + _lockDuration)It's possible for the new lock end time to be earlier than the original lock end time. This applies as well to the
lockAssetOnBehalf()function added in PR#20.Recommendation
Use the maximum of the current
lockEndTimeandblock.timestamp + _lockDurationas the updatedlockEndTime.
Low Risk6 findings
Granting multiple roles to the same address is risky
Summary
Granting multiple roles, which have different levels/types of authorization, to the same address is risky because it defeats the motivation for role based access control to impose separation of privileges.
Finding Description
Different protocol components define different roles to enforce role based access control. However, all the roles are initialized to a single address/account. This defeats the purpose of role based access control:
- NUSD assigns
_adminto bothDEFAULT_ADMIN_ROLEandDENYLIST_MANAGER_ROLE. - sNUSD assigns
_ownerto bothDEFAULT_ADMIN_ROLEandPAUSER_ROLE. - Router assigns
_admintoDEFAULT_ADMIN_ROLE,WHITELISTER_ROLE, andPAUSER_ROLE. - AssetLock assigns
_admintoDEFAULT_ADMIN_ROLE,MANAGER_ROLE, andPAUSER_ROLE. - YieldDistributor assigns
_adminto bothDEFAULT_ADMIN_ROLEandYIELD_TOKEN_MANAGER_ROLE.
These roles have different levels/types of privileges and should be granted to different actors.
DEFAULT_ADMIN_ROLE, which has the highest level of privilege should not be given to addresses that also control lower privileged roles because their risk profiles are different which allows their wallet operational security to be managed proportionately.Impact Explanation
High, because if that one account is compromised then protocol functionality controlled by all its roles is compromised.
Likelihood Explanation
Very low, assuming that single account has the highest level of operational security and is not compromised.
Recommendation
Consider using different addresses/accounts to initialize different roles within the protocol components.
Neutrl
Spearbit
Reviewed that PR #31 removes multiple assignments of roles to the same address and instead moves those role assignments to the deployment script where different addresses (dummy zero addresses for now which are noted to be replaced later) are assigned to these different roles.
- NUSD assigns
Removed yield tokens can never be added again affecting protocol yield strategy
Summary
Removed yield tokens from
YieldDistributorcan never be added again, which negatively affects protocol yield strategy and distribution thereafter.Finding Description
YieldDistributormanages and distributes yield from accepted stablecoins (USDC, USDT and USDe) to sNUSD holders. While the yield generation happens offchain, the generated yield in USDC, USDT and USDe is converted to NUSD viaconvertYieldToNUSD()and then distributed to sNUSD holders viadistributeYield().The protocol manages the accepted yield tokens via
addYieldToken()andremoveYieldToken().addYieldToken()prevents addition of duplicated yield tokens by checking if the one being added already exists in theyieldTokensarray.removeYieldToken()removes a yield token not by removing it from the array but only marking it as inactive viayieldTokens[i].isActive = false.Given this differing logic during addition and removal, once a yield token is removed by marking it as inactive, it can never be added back again because
addYieldToken()only checks for its presence and not forisActive == true.Impact Explanation
Low, because not being able to add a yield token back will negatively impact protocol yield strategy and distribution thereafter.
Likelihood Explanation
Low, assuming it is unlikely for the admin to remove a yield token and then want to add it back later for some reason.
Recommendation
Consider checking for
yieldTokens[i].isActive == truealong withyieldTokens[i].token == _yieldTokeninaddYieldToken()logic, so that:- If a token is present but
isActive == falsethen it can be set totrue. - If a token is present and
isActive == truethen it can revert. - If a token is absent then it can be added.
- If a token is present but
Using a single EOA deployer for all privileged roles across protocol is risky
Summary
DeployProtocolscript currently uses an EOA deployer address for all privileged roles across the protocol, which is extremely risky.Finding Description
DeployProtocolscript derives a deployer address from a private key read from an environment variable and uses that in calls todeployDeterministic(deployer)anddeployOthers(deployer).deployDeterministic(deployer)uses that deployer address to initialize all privileged roles across NUSD and sNUSD contracts.deployOthers(deployer)uses the same deployer address to initialize all privileged roles acrossRouter,StableMinter,Redeemer,AssetReserve,AssetLock, andYieldDistributorcontracts.Impact Explanation
High, because if that single EOA is compromised, the entire protocol can be critically impacted given the centralized roles and responsibilities as documented in Finding 5.
Likelihood Explanation
Very low, assuming that this is a placeholder for now, will be replaced by a safer alternative and transferred immediately after deployment with a combination of multisig/mpc wallets (as communicated).
Recommendation
Consider:
- Using safer alternatives than reading the deployer key from an environment variable as described in Foundry's key management best practices.
- Transferring the EOA deployer to a combination of multisig/mpc wallets immediately after deployment.
- Revoking all access from the EOA deployer immediately (and atomically) after step (2).
Neutrl
Spearbit
Reviewed that PR #30 adds a
mpcAdminWallet(dummy zero-address for now noting that this will be replaced by the actual MPC admin wallet once its setup is complete) to which the admin roles of all deployed contracts are immediately transferred from the earlier used deployer address inrun().Users can take advantage of share values changing suddenly when the vesting period is increased
State
- Fixed
PR #32
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Medium Submitted by
Kurt Barry
Description
The
setVestingPeriod()function checks that thegetUnvestedAmount()function returns zero and reverts if not. However, an increase of the vesting period can still affect the unvested rewards, because neithervestingAmountnorlastDistributionTimestampare updated. This will lead to a discontinuous change in the return of thetotalAssets()function and thus affect the value per share. Specifically, if the vesting period is lengthened, and the new value oflastDistributionTimestamp + vestingPeriodis in the future, a fraction of thevestingAmountvalue will become unvested again.Users can take advantage of this as follows:
- Withdraw just prior to a vesting period increase. This will withdraw some amount of the vested rewards.
- Redeposit after the increase, re-earning a portion of the previously vested rewards at the expense of other users.
Proof of Concept
The following test can be added to
test/unit/concrete/sNUSD/authorized/sNUSD_authorized.t.soland run viaforge test --match-test test_ChangingVestingPeriodChangesUnvestedRewards:function test_ChangingVestingPeriodChangesUnvestedRewards() external { address user1 = address(0x1111); address user2 = address(0x2222); deal(address(nusd), user1, 1 ether); deal(address(nusd), user2, 1 ether); // Both users deposit. vm.startPrank(user1); nusd.approve(address(sNusd), type(uint256).max); sNusd.deposit(1 ether, user1); vm.stopPrank(); vm.startPrank(user2); nusd.approve(address(sNusd), type(uint256).max); sNusd.deposit(1 ether, user2); vm.stopPrank(); // First transfer to create unvested rewards vm.startPrank(rewarder); deal(address(nusd), rewarder, 1 ether); nusd.approve(address(sNusd), 1 ether); sNusd.transferInRewards(1 ether); vm.stopPrank(); uint256 vestingPeriod = sNusd.vestingPeriod(); vm.warp(sNusd.lastDistributionTimestamp() + vestingPeriod); // All rewards have vested. assertEq(sNusd.getUnvestedAmount(), 0); // The two users, having made identical deposits at the identical time, have equal assets. assertEq(sNusd.convertToAssets(sNusd.balanceOf(user1)), sNusd.convertToAssets(sNusd.balanceOf(user2))); // user1 withdraws just before the vesting period update // (for simplicity, we are in instant withdrawal mode) vm.startPrank(users.admin); sNusd.setCooldownDuration(0); vm.stopPrank(); vm.startPrank(user1); sNusd.redeem(sNusd.balanceOf(user1), user1, user1); vm.stopPrank(); uint256 totalAssetsBefore = sNusd.totalAssets(); vm.startPrank(users.admin); sNusd.setVestingPeriod(2 * vestingPeriod); vm.stopPrank(); // This should still be zero but instead it has gone back to half of the unvestedRewards value. assertEq(sNusd.getUnvestedAmount(), 0.5e18); // Total assets has changed discontinuously assertEq(totalAssetsBefore - 0.5e18, sNusd.totalAssets()); // user1 redeposits, including their accrued interest, to take advantage of the devalued share price. vm.startPrank(user1); sNusd.deposit(nusd.balanceOf(user1), user1); vm.stopPrank(); // Warp to end of the new vesting period. vm.warp(sNusd.lastDistributionTimestamp() + 2 * vestingPeriod); // user1, despite merely withdrawing and re-depositing, and with no new rewards added, // now has significantly more assets than user2, because the previously added rewards have // been redistributed in their favor. assertEq(sNusd.convertToAssets(sNusd.balanceOf(user1)), 1799999999999999998); assertEq(sNusd.convertToAssets(sNusd.balanceOf(user2)), 1200000000000000001); }Recommendation
Set
vestingAmountto zero whenever the vesting period is updated (or just when it is lengthened, as this issue does not apply to shortening the vesting period).If coolDownDuration is lowered, users may be able to reduce their cooldown end times
State
- Fixed
PR #33
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
Kurt Barry
Description
Both
sNUSD.cooldownAssets()andsNUSD.cooldownShares()update the user's cooldown end time as follows:cooldowns[msg.sender].cooldownEnd = uint104(block.timestamp) + cooldownDuration;This overwrites the previous value. If the
cooldownDurationis lowered soon enough after a user initiates a cooldown, then the user may be able to lower theircooldownEndvalue by initiating another cooldown (potentially depositing more assets if need be).Recommendation
Set the
cooldownEndvalue to the maximum of the current value anduint104(block.timestamp) + cooldownDurationin bothcooldownAssets()andcooldownShares().Yield accrual in YieldDistributor will fail if NUSD mint limit is exceeded
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Chinmay Farkya
Description
convertYieldtoNUSD()function inYieldDistributor.solis used to accrue allyieldTokenbalances. It first calculates the total amount in terms of NUSD using collateral prices fromROUTER.quoteDeposit()and then calls the ROUTER to mint the calculated NUSD amounts for each yieldToken.The call flow is
convertYieldtoNUSD() => ROUTER.mint() => StableMinter.mint()StableMinter.solinherits fromBaseMintRedeem.solwhich does some checks on this mint order parameters. One such check is_belowMaxMintPerBlock().function _belowMaxMintPerBlock(uint256 mintAmount) internal view { if (mintedPerBlock[block.number] + mintAmount > maxMintPerBlock) { revert MaxMintPerBlockExceeded(); } }This check ensures that the already minted NUSD in the actual block plus the amount to be minted is below the
maxMintPerBlockvalue. This mint limit is shared by all users that are trying to mint NUSD from the minter contract of that particular collateral asset.The
convertYieldtoNUSD()loops over all yieldTokens, so the whole yield distribution can fail if mint limit of any one collateral asset's minter has been exceeded.This can cause yield distribution failure, and delay in accruing yield to sNUSD in a timely manner. This can also be purposefully DOS'ed by an attacker.
This will be problematic for integrators that depend on timely yield accrual as any withdrawals from sNUSD will still succeed even though the yield that was expected at the time was purposefully delayed by an attacker => leading to potential loss of yield even though they contributed with their NUSD properly.
Recommendation
Consider documenting this potential threat for integrators and monitoring mint usage over time to dynamically adjust mint limits such that yield operations do not fail.
Neutrl
Acknowledged.
We acknowledge this potential scenario where yield distribution could be disrupted by attackers exhausting mint limits for specific collateral assets.
If this situation occurs, we have several mitigation strategies available:
- Whitelist Management
- Since we operate with whitelisting controls, we can remove malicious actors from the whitelist to prevent further abuse
- Dynamic Threshold Adjustment
- We can increase the maxMintPerBlock threshold for affected collateral assets to ensure yield distribution operations can proceed
- Operational Monitoring
- We will monitor mint usage patterns to proactively identify and address potential DoS attempts before they impact yield distribution
These mitigation measures provide us with the operational flexibility to maintain timely yield accrual while protecting against malicious interference.
Spearbit
Acknowledged.
Informational17 findings
Missing event emission for privileged functions reduces offchain transparency
Description
While many of the privileged functions across the protocol emit events to enable offchain monitoring and increased transparency, some are missing such event emits. This reduces offchain transparency, which is a key aspect of Neutrl protocol to strengthen user confidence.
Recommendation
Consider emitting missed events in all privileged functions.
Unused code constructs reduce readability
Description
There are some code constructs such as declared errors and events which are unused and reduce readability.
Recommendation
Consider revisiting their intended usage or remove them for clarity.
Centralized roles and responsibilities across the protocol are susceptible to misuse
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
The protocol has several centralized roles and responsibilities across components, which are susceptible to misuse/abuse if compromised, such as:
- NUSD
MINTER_ROLEcan mint arbitrary NUSD amounts to arbitrary addresses. - NUSD
REDEEMER_ROLEcan burn arbitrary NUSD amounts from arbitrary addresses. - NUSD
DENYLIST_MANAGER_ROLEcan prevent arbitrary NUSD transfers. - Router
WHITELISTER_ROLEcan prevent arbitrary mint/redeem requests. - Router
KEEPER_ROLEcan serve/cancel arbitary mint/redeem requests. - Router admin can configure arbitrary minters/redeemers.
- Router admin can set arbitrary order waiting periods affecting order expiry.
- Minter/Redeemer admin can add/remove support for arbitrary assets.
- Minter/Redeemer admin can set arbitrary min/max price for supported collateral assets.
- Minter/Redeemer admin can set arbitrary max mints/redeems per block.
- Admins can set arbitrary asset reserve and router addresses.
PAUSER_ROLEcan pause/unpause various key functions.- YieldDistributor admin can add/remove support for arbitrary yield tokens.
- YieldDistributor admin can recover arbitrary ERC20 tokens.
- sNUSD
BLACKLIST_MANAGER_ROLEcan prevent arbitrary sNUSD transfers. - sNUSD admin can set arbitrary cooldown durations for unstaking/withdrawing.
- In PR#20, the
MANAGER_ROLEcan indefinitely extend a user's lock end time by locking a tiny amount of tokens just before each lock expiration while specifying the largest possible duration to update the end time far into the future.
Recommendation
Consider:
- Enforcing the best privilege separation possible across roles/actors.
- Implementing the highest degree of operational security for wallet accounts controlling them.
- Implementing timelocks for some of the most critical actions.
Neutrl
Acknowledged.
We acknowledge the centralized roles identified in the protocol and recognize the potential security risks associated with compromised administrative keys. To mitigate the risk of compromised admin keys, we will implement the following security measures:
Security Mitigation Strategy
-
Robust Private Key Management
-
Strict operational security (OpSec) practices for all admin wallets and role-based wallets
-
Industry-standard key storage and access protocols
-
Multi-Party Computation (MPC) Wallet Implementation
-
All administrative roles managed through MPC wallets with strict role-based policies
-
Multi-signature requirements for critical operations
-
Advanced Transaction Protection
-
Integration with third-party security solutions to detect and automatically reject malicious transactions on MPC wallets
-
Real-time monitoring and anomaly detection for administrative actions
These comprehensive security measures will effectively mitigate the risks associated with centralized roles while maintaining the operational capabilities necessary for protocol functionality.
Spearbit
Acknowledged.
Event parameters can be indexed for efficient lookups
Description
Events
LockedAmountRedistributed,UnstakedandRequestUnstakeddo not use indexed attribute on their address parameters for more efficient lookups.Recommendation
Consider adding
indexedattribute to these event parameters.Symbol instead of name passed to ERC20Permit constructor in sNUSD
State
- Fixed
PR #25
Severity
- Severity: Informational
Submitted by
Kurt Barry
Description
The name, rather than the symbol, is intended to be passed to the
ERC20Permitconstructor:However, the
sNUSDcontract passes the symbol. This is in contrast to theNUSDcontract, which correctly uses the token name.Recommendation
Pass the name (
"Staked NUSD") rather than the symbol to theERC20Permitconstructor insNUSD.Missing zero-address checks on critical protocol addresses
Description
While the protocol implements zero-address checks in several places, this is missing in some of the initializations and setter functions.
Recommendation
Consider adding zero-address checks on all critical protocol addresses to reduce accidental errors.
Miscellaneous issues reduce readability
Description
There are miscellaneous minor issues related to naming, NatSpec, function visibility, missing checks etc. across the codebase that reduce readability.
Recommendation
Consider:
- Adding a non-zero check for
amountToDistributeinredistributeLockedAmount(). - Making
5e18a declared constant or a settable storage value to allow modifying the acceptable slippage inconvertYieldToNUSD(). - Renaming
startTimeaslockStartTimeinAssetLock.UserLock. - Renaming
_assetas_collateralAssetin various Router functions. - Making
Router.quoteRedemption()externalvisibility instead ofpublic. - Making
SingleAdminAccessControl.owner()externalvisibility instead ofpublicas in IERC5313. - Changing
@noticeof_setMintWhitelisted()to/// @notice Internal function to set a user's mint whitelist status. - Changing
@noticeof_setMintWhitelistEnforcement()to/// @notice Internal function to set the mint whitelist enforcement. - Adding
@paramfor_nusdinRouter.constructor. - Adding
@paramfor_multisiginElpDistribution.constructor. - Using named mapping parameters for Router
mintersandredeemers.
- Adding a non-zero check for
Use _msgSender() instead of msg.sender in SingleAdminAccessControl
State
- Fixed
PR #36
Severity
- Severity: Informational
Submitted by
Kurt Barry
Description and Recommendation
Consider using
_msgSender()instead ofmsg.senderinSingleAdminAccessControlfor consistency with the OZ base class implementation:While currently the two options are equivalent, if there's ever a contract that inherits
SingleAdminAccessControland overrides_msgSender(), then bugs could result from usingmsg.senderinSingleAdminAccessControlinstead of_msgSender().Unused imports
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Kurt Barry
Description
The following imports are unused:
- StableMinter.sol#L4-L5: the
IERC20andSafeERC20imports are only used in theusingstatement on line 15; they can be removed along with theusingstatement. - Redeemer.sol#L5: the
SafeERC20import is only used in theusingstatement on line 14; it can be removed along with theusingstatement.
Recommendation
Remove unused imports and related lines for clarity and readability.
Consider additional validation checks
State
- Fixed
PR #39
Severity
- Severity: Informational
Submitted by
Kurt Barry
Description and Recommendation
- In
Router.configureAsset(), consider validating that_assetis not NUSD as NUSD should never be its own collateral. - In
Router._setMinter()andRouter._setRedeemer(), consider verifying that the minter or redeemer supports the specified asset. - In
BaseMintRedeem._addAsset(), consider validating that_assetis not NUSD as NUSD should never be its own collateral.
Misleading benefactor value in Redeem event when a queued redemption request is served
State
- Fixed
PR #37
Severity
- Severity: Informational
Submitted by
Kurt Barry
Description
When a queued redemption request is served, the
order.benefactorvalue is changed to be theRoutersince that's where the NUSD tokens were transferred when the request was created. This modification is necessary for the internal_redeem()function to work correctly, but it obscures the original benefactor in the emittedRedeemevent--it will now be theRouterinstead of the original NUSD source. This makes this event parameter less informative for an asynchronously executed redemption.Recommendation
Consider modifying the code to always emit the
Redeemevent using the original benefactor.Yield should be accrued before removing a yieldToken from YieldDistributor
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Chinmay Farkya
Description
The
YieldDistributorcontract has methods to accrue and distribute yield according to yield token balances accumulated in the contract. TheYIELD_TOKEN_MANAGER_ROLEis supposed to periodically callconvertYieldToNUSD()to convert all token balances into NUSD.There is also a way to remove a registered yield token from the distribution mechanisms, via the
removeYieldToken()function which is controlled byDEFAULT_ADMIN_ROLE.The problem here is that if the yield token is removed from the
yieldTokensarray without accruing its existing balance into NUSD yield, then the existing balance will be left out of the distribution mechanism of the sNUSD contract.The
yieldTokencan also not be added again because of another issue, and can only be recovered. SinceYIELD_TOKEN_MANAGER_ROLEandDEFAULT_ADMIN_ROLEare assumed to be separate privileged addresses, this can break the yield distribution process.Recommendation
Protocol has to make sure that these two calls :
convertYieldToNUSD()andremoveYieldToken()are always made in the specified order by privileged roles.Incorrect comments
State
- Fixed
PR #38
Severity
- Severity: Informational
Submitted by
Chinmay Farkya
Description
There are several instances of incorrect/ incomplete comments across the codebase :
- sNUSD.sol#L177 : "This function is called by the
StakingRewardsDistributor contract" should be changed to "This function is called by theYieldDistributorcontract". - YieldDistributor.sol#L17: "A contract that manages and distributes yield from accepted stablecoins (USDC & USDT) to sNUSD holders" should be changed to "A contract that manages and distributes yield from accepted stablecoins (USDC,USDT & USDe) to sNUSD holders".
- BaseMintRedeem.sol#L117-L118: these two comment lines should use the term
priceinstead ofslippage; while the terms are logically related, "slippage" usually denotes a relative price difference rather than an absolute price (which is what is actually accepted as the argument to this function and stored in the contract).
Recommendation
Consider modifying the mentioned comments.
AUTHORIZED role is not set up in AssetReserve constructor
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Chinmay Farkya
Description
Across the codebase, all roles are set up in the constructor, which ensures proper role allotment at deployment.
In one instance in
AssetReservecontract, theAUTHORIZEDrole has not been granted by default. This role is required to move collateral assets from AssetReserve contract to custodian addresses.This can lead to no address having the
AUTHORIZEDrole initially, affecting protocol strategy as user funds sit inAssetReservecontract.Recommendation
Consider granting
AUTHORIZEDrole at deployment via constructor, for consistency.Neutrl
Acknowledged.
Most of the roles have been removed according to Finding #1. They are defined at deployment, for the authorized role, we are aware of this we need first some MPC wallets being created.
Spearbit
Acknowledged.
Pending request listing functions include expired requests in the returned arrays
State
- Fixed
PR #26
Severity
- Severity: Informational
Submitted by
Kurt Barry
Description
In the
Routercontract the functionsgetPendingMintRequests()andgetPendingRedeemRequests()both include pending but expired requests in the arrays they return. However, expired requests cannot be served, only cancelled. Depending on the use cases for these functions, including expired requests may be undesirable and lead to confusion.Recommendation
Consider whether including expired requests is desirable. For maximum flexibility, a boolean parameter could be added to these functions specifying whether to include expired requests or not.
Subcontract-specific constructs do not belong in base contracts
State
- Fixed
PR #42
Severity
- Severity: Informational
Submitted by
Kurt Barry
Description
The
BaseMintRedeemcontract defines a number of fields and functions that are specific to whether the derived contract is a minter or redeemer; in either case, the fields for the other type of derived contract are entirely unused in the derived contract. Specifically, the fieldsminPrice,maxPrice,mintedPerBlock,redeemedPerBlock,maxMintPerBlock, andmaxRedeemedPerBlock, and the associated functions that set or check these values, are all minter- or redeemer-specific.This is undesirable for several reasons:
- Gas costs. Derived contracts possess all of these fields and functions. This increases bytecode size (hence deployment costs), and can increase function resolution costs at runtime.
- Possibility for mistakes. It creates the possibility of accidentally calling the wrong function and having a transaction succeed when the intended goal was not accomplished. Concretely, imagine a governance action that is supposed to set
minPriceon the minter andmaxPriceon the redeemer, but accidentally swaps which contract each field is set on. The action will succeed with no obvious indication that a mistake was made, whereas it would fail instantly if minters and redeemers didn't share functions unnecessarily. - Maintainability. It violates expectations of best practices, making the code less readable and maintainable.
Recommendation
Since the desire appears to be capable of supporting multiple minters and redeemers in the future, refactor the inheritance structure, with an extra layer of base contracts that are specific to minters and redeemers:
. BaseMinter -- all minter contracts /BaseMintRedeem \ BaseRedeemer -- all redeemer contractsMove the minter- and redeemer-specific fields and functions to the appropriate intermediate base contract.
Inconsistent use of errors in BaseMintRedeem and derived contracts
State
- Fixed
PR #40
Severity
- Severity: Informational
Submitted by
Kurt Barry
Description
The
BaseMintRedeemclass defines a custom errorZeroInput()but usesInvalidInput()for cases where the input is zero; only in the derived classRedeemerisZeroInput()is used, for the exact same sort of error thatBaseMintRedeemis usingInvalidInput()for.Recommendation
Consider making the error usage more regular and consistent with the rest of the codebase. In particular, use
ZeroInput()when the input is zero, andInvalidInput()for other problems with function arguments.
Gas Optimizations6 findings
mintRequestId and redemptionRequestId can be cached
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
0xRajeev
Description
_requestMint()and_requestRedemption()loadmintRequestIdandredemptionRequestIdfrom storage three times consecutively. Given these are frequently used user flows, these repeated storage reads unnecessarily leads to more gas usage than required.mintRequests[mintRequestId] = _order;mintRequestStatus[mintRequestId] = RequestStatus.PENDING;emit RequestMint(mintRequestId, _order);redemptionRequests[redemptionRequestId] = _order;redemptionRequestStatus[redemptionRequestId] = RequestStatus.PENDING;emit RequestRedemption(redemptionRequestId, _order);Recommendation
Consider caching these storage variables to avoid repeated storage reads.
getPendingMintRequests() and getPendingRedemptionRequests() may hit OOG exception
Description
getPendingMintRequests()andgetPendingRedemptionRequests()loop through every mint and redemption requests made in the protocol lifetime to check which of them are pending to return their details.While this may work initially, this iterative approach is likely to hit Out-of-Gas (OOG) exception at some point in the future when the number of requests exceed a certain number. Even if this is only used by the protocol for offchain monitoring and accounting, the logic is suboptimal for gas usage.
Recommendation
Consider:
- Adding a starting index parameter to these functions so that successive calls can optionally ignore the previously determined pending requests.
- Caching
mintRequestIdandredemptionRequestIdinstead of loading them repeatedly from storage.
Neutrl
Spearbit
Reviewed that PR#26 mitigates the issue as recommended in (1) by adding another set of functions that take starting index as a parameter. All functions now also return only pending mint/redemption requests that expire beyond
block.timestamp. This PR however introduced an issue ingetPendingRedemptionRequests()functions wheremintRequests[i].expirywas incorrectly used instead ofredemptionRequests[i].expiry. This was later resolved in PR#44.Redundant order type checks in mint() and redeem()
Description
mint()andredeem()check their order types to beOrderType.MINTandOrderType.REDEEMrespectively. However, they call_verifyOrder(order)which also checks forif (order.orderType != OrderType.MINT && order.orderType != OrderType.REDEEM) revert InvalidInput().These checks are redundant and the consolidated check in
_verifyOrder()can be removed in favor of the specific checks already performed at the two call sites.Recommendation
Consider:
- Removing
if (order.orderType != OrderType.MINT && order.orderType != OrderType.REDEEM) revert InvalidInput()from_verifyOrder() - Moving the duplicated check
if (order.expiry < block.timestamp) revert OrderExpired()frommint()andredeem()to_verifyOrder().
Neutrl
Spearbit
Reviewed that PR #28 fixes the issue as recommended in (1).
- Removing
Redundant getters
State
- Fixed
PR #34
Severity
- Severity: Gas optimization
Submitted by
Kurt Barry
Description
The
getAssetInfo()andgetUserLock()getters in theAssetLockcontract are redundant with thepublicfieldsassetInfoanduserLocks. Having both increases bytecode size (and thus deployment costs) and can also increase function dispatch costs on calls to the contract since the function lookup logic will be more extensive.Recommendation
Either remove the unnecessary getters or make the associated fields
private.Unnecessary storage field
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
Kurt Barry
Description
The
isSupportedfield of theAssetInfostruct used in theAssetLockcontract isn't strictly necessary--the same effect could be achieved by allowing amaxLockCapacityof zero denote an unsupported asset. This would eliminate a full storage slot per locked position.Recommendation
Remove
AssetLock.isSupportedand rely onmaxLockCapacityalone to reduce gas costs.Unnecessary transfer of sNUSD shares in ELPDistribution.proceedToELPDistribution()
State
- Fixed
PR #41
Severity
- Severity: Gas optimization
Submitted by
Kurt Barry
Description and Recommendation
In
ELPDistribution.proceedToELPDistribution(), funds are deposited into thesNUSDcontract and then the resulting shares are transferred to a multisig address for storage:sNUSD.deposit(totalAllocated, address(this));sNUSD.transfer(multisig, totalAllocated);The second parameter of
deposit()is the address that receives the shares, so the code could be simplified to:sNUSD.deposit(totalAllocated, multisig);eliminating an external call and thus saving gas.