Omniscia Nexera Audit

PurchaseDiscountGenesisIdsMixerStorage Manual Review Findings

PurchaseDiscountGenesisIdsMixerStorage Manual Review Findings

PDI-01M: Inexistent Restriction of Selectors

TypeSeverityLocation
Logical FaultPurchaseDiscountGenesisIdsMixerStorage.sol:
I-1: L170, L172
I-2: L192, L194

Description:

The discount application and price per fraction evaluation selectors can be arbitrarily set in the configuration of the purchase discount facet, potentially allowing malicious access to different internally-invoked functions of the tokenizer's Diamond.

Impact:

As the encodedData and selector of a particular discount application or price evaluation are arbitrarily specified user data, it is possible for a malicious campaign creator to introduce a backdoor to their campaign to gain access to functions they otherwise would not be able to invoke, especially via the PurchaseDiscountGenesisIdsMixerStorage::getPricePerFractionForGenesisId pathway which restricts only two arguments.

Example:

packages/contracts/contracts/internalFacets/purchasePhaseFacets/purchaseDiscountFacets/genesisIds/PurchaseDiscountGenesisIdsMixerStorage.sol
131function checkValidityAndStoreSelectors(
132 Layout storage l,
133 uint256 length,
134 uint256 currentId,
135 bytes4[] memory setAndCheckSelectors,
136 bytes4[] memory applyDiscountSelectors,
137 bytes4[] memory getPriceSelectors,
138 bytes[] memory setAndCheckData
139) private {
140 for (uint256 i; i < length; ) {
141 bytes4 setAndCheckSelector_ = setAndCheckSelectors[i];
142
143 if (!l.isSetAndCheckSelectorSupported[setAndCheckSelector_]) revert UnsupportedSelector();
144
145 bytes memory callDataInput = abi.encodeWithSelector(setAndCheckSelector_, setAndCheckData[i]);
146 (bool success, bytes memory result) = address(this).call(callDataInput);
147
148 if (!success) {
149 assembly ("memory-safe") {
150 revert(add(result, 32), mload(result))
151 }
152 }
153
154 l.isDiscountModelForId[currentId][applyDiscountSelectors[i]] = true;
155 l.isGetPriceModelForId[currentId][getPriceSelectors[i]] = true;
156
157 unchecked {
158 i += 1;
159 }
160 }
161}
162
163function applyDiscount(
164 Layout storage l,
165 uint256 campaignId,
166 uint256 amountOfFractions,
167 address account,
168 bytes calldata operationData
169) internal returns (uint256, uint256) {
170 (bytes4 selector, bytes memory encodedData) = abi.decode(operationData, (bytes4, bytes));
171
172 if (!l.isDiscountModelForId[campaignId][selector]) revert UnsupportedDiscountModel();
173
174 bytes memory callDataInput = abi.encodeWithSelector(selector, campaignId, amountOfFractions, account, encodedData);
175 (bool success, bytes memory returnData) = address(this).call(callDataInput);
176
177 if (!success) {
178 assembly ("memory-safe") {
179 revert(add(returnData, 32), mload(returnData))
180 }
181 }
182
183 return abi.decode(returnData, (uint256, uint256));
184}

Recommendation:

We advise the PurchaseDiscountGenesisIdsMixerStorage::checkValidityAndStoreSelectors function to restrict the applyDiscountSelectors and getPriceSelectors that are authorized in the system, disallowing corruption of a particular campaign via unauthorized access of internally-invokeable functions.

Alleviation (d682057ecb0e254069773d64f32c068cedb71e2a):

The code has been updated to restrict the applyDiscountSelector_ and getPriceSelector_ values to specific implementations within the PurchaseDiscountGenesisIdsMixerStorage::checkValidityAndStoreSelectors function, preventing the backdoor invocation pathways described within this exhibit.