nuklai / nuklai-contracts Goto Github PK
View Code? Open in Web Editor NEWSmart Contracts for the Data Tunnel Protocol
Smart Contracts for the Data Tunnel Protocol
Type | Severity | Location |
---|---|---|
Logical Fault | GenericSingleDatasetSubscriptionManager.sol:L6, L18 |
The GenericSingleDatasetSubscriptionManager
contract is meant to be an upgradeable contract, however, it utilizes non-upgradeable variants of its OpenZeppelin dependency tree.
As a result, derivative contracts such as ERC20SubscriptionManager
are unable to initialize the ERC721Enumerable
implementation properly.
The ERC20SubscriptionManager
deployment will contain an empty EIP-721 name and symbol as its dependency is initialized solely in the logic implementation and not in the upgradeable implementation.
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {ISubscriptionManager} from "../interfaces/ISubscriptionManager.sol";
import {IDatasetNFT} from "../interfaces/IDatasetNFT.sol";
/**
* @title GenericSingleDatasetSubscriptionManager contract
* @author Data Tunnel
* @notice Abstract contract serving as the foundation for managing single Dataset subscriptions and related operations.
* Derived contracts mint ERC721 tokens that represent subscriptions to the managed Dataset, thus, subscriptions
* have unique IDs which are the respective minted ERC721 tokens' IDs.
* @dev Extends ISubscriptionManager, Initializable, ERC721Enumerable
*/
abstract contract GenericSingleDatasetSubscriptionManager is ISubscriptionManager, Initializable, ERC721Enumerable {
We advise the code as a whole to make user of upgradeable OpenZeppelin contracts, ensuring that the ERC20SubscriptionManager
can properly initialize the ERC721Enumerable
contract in its ERC20SubscriptionManager::initialize
function.
Type | Severity | Location |
---|---|---|
Logical Fault | GenericSingleDatasetSubscriptionManager.sol:L300, L317 |
A GenericSingleDatasetSubscriptionManager::_extendSubscription
operation permits a subscription to be extended beyond the 365
day limit a subscription duration should abide by.
In detail, it can exceed the 365
day limit by a maximum of 30
days, leading to the actual maximum duration achievable as being 395
days.
A subscription's duration can presently exceed the 365
limit enforced by its creation via the extension mechanism the contract exposes.
function _extendSubscription(uint256 subscription, uint256 extraDurationInDays, uint256 extraConsumers) internal {
_requireMinted(subscription);
if (extraDurationInDays > 365) revert SUBSCRIPTION_DURATION_INVALID(1, 365, extraDurationInDays);
SubscriptionDetails storage sd = _subscriptions[subscription];
uint256 newDurationInDays;
uint256 newValidSince;
uint256 currentFee;
if (sd.validTill > block.timestamp) {
// Subscription is still valid but remaining duration must be <= 30 days to extend it
if (extraDurationInDays > 0)
if ((sd.validTill - block.timestamp) > 30 * 1 days)
revert SUBSCRIPTION_REMAINING_DURATION(30 * 1 days, (sd.validTill - block.timestamp));
// (sd.validTill - sd.validSince) was enforced during subscription to be an integral multiple of a day in seconds
uint256 currentDurationInDays = (sd.validTill - sd.validSince) / 1 days;
(, currentFee) = _calculateFee(currentDurationInDays, sd.paidConsumers);
newValidSince = sd.validSince;
newDurationInDays = currentDurationInDays + extraDurationInDays;
} else {
// Subscription is already invalid
// currentFee = 0;
newValidSince = block.timestamp;
newDurationInDays = extraDurationInDays;
}
uint256 newConsumers = sd.paidConsumers + extraConsumers;
(, uint256 newFee) = _calculateFee(newDurationInDays, newConsumers);
if (newFee <= currentFee) revert NOTHING_TO_PAY();
_charge(_msgSender(), newFee - currentFee);
sd.validSince = newValidSince;
sd.validTill = newValidSince + (newDurationInDays * 1 days);
sd.paidConsumers = newConsumers;
emit SubscriptionPaid(subscription, sd.validSince, sd.validTill, sd.paidConsumers);
}
We advise the extension code of GenericSingleDatasetSubscriptionManager::_extendSubscription
to validate the newDurationInDays
value rather than the extraDurationInDays
value when enforcing the subscription duration limit, ensuring that a subscription can at most have a duration of 365
days.
Type | Severity | Location |
---|---|---|
Language Specific | DistributionManager.sol:L196 |
The DistributionManager::receivePayment
function does not prevent native funds from being sent alongside EIP-20 assets, leading to a potential loss of funds that are transmitted in such a call.
It is presently possible to lock native funds in the DistributionManager
contract by specifying a non-zero amount of native funds alongside a DistributionManager::receivePayment
call with an EIP-20 asset.
if (address(token) == address(0)) {
if (amount != msg.value) revert MSG_VALUE_MISMATCH(msg.value, amount);
} else {
IERC20(token).safeTransferFrom(_msgSender(), address(this), amount);
}
We advise the code to ensure that the msg.value
is 0
in case the token
specified is non-zero, ensuring that native funds cannot be accidentally locked in the contract when supplying native funds alongside an EIP-20 DistributionManager::receivePayment
function execution.
Type | Severity | Location |
---|---|---|
Gas Optimization | DatasetNFT.sol:L192 |
The linked for
loop increments / decrements the iterator "safely" due to Solidity's built-in safe arithmetics (post-0.8.X
).
for (uint256 i; i < models.length; i++) {
We advise the increment / decrement operation to be performed in an unchecked
code block as the last statement within the for
loop to optimize its execution cost.
Type | Severity | Location |
---|---|---|
Gas Optimization | DatasetNFT.sol:L283, L284 |
The linked input arguments are set as memory
in external
function(s).
function proposeManyFragments(
uint256 datasetId,
address[] memory owners,
bytes32[] memory tags,
bytes calldata signature
) external {
We advise them to be set as calldata
optimizing their read-access gas cost.
Type | Severity | Location |
---|---|---|
Gas Optimization | FragmentNFT.sol:L277, L278 |
The linked input arguments are set as memory
in external
function(s).
function proposeMany(
address[] memory owners,
bytes32[] memory tags_,
bytes calldata signature
) external onlyDatasetNFT {
We advise them to be set as calldata
optimizing their read-access gas cost.
Type | Severity | Location |
---|---|---|
Language Specific | VerifierManager.sol:L26, L27 |
The FragmentPending
, and FragmentResolved
events of the VerifierManager
contract do not contain any indexed
arguments.
event FragmentPending(uint256 id);
event FragmentResolved(uint256 id, bool accept);
We advise them to be introduced, particularly in relation to the id
to ensure off-chain processes can optimally filter these events.
Type | Severity | Location |
---|---|---|
Language Specific | GenericSingleDatasetSubscriptionManager.sol:L133, L310, L311, L328 |
The linked mathematical operations are guaranteed to be performed safely by surrounding conditionals evaluated in either require
checks or if-else
constructs.
return (newFee > currentFee) ? (newFee - currentFee) : 0;
Given that safe arithmetics are toggled on by default in pragma
versions of 0.8.X
, we advise the linked statements to be wrapped in unchecked
code blocks thereby optimizing their execution cost.
Type | Severity | Location |
---|---|---|
Logical Fault | DistributionManager.sol:L160-L178 |
The DatasetNFT::setManagers
function permits the subscriptionManager
, distributionManager
, and verifierManager
implementations that the system will utilize to be arbitrarily specified which is a significant security flaw in the system given that these modules are expected to utilize user funds, get approved by users to spend funds, and generally act as integral modules to the DatasetNFT
contract's overall operation.
Arbitrarily specified subscription and distribution implementations can lead to direct fund loss (by exploiting approvals) or to "free" data-set contributions (as the distributor could f.e. distribute all funds to a single party).
function _setDatasetOwnerPercentage(uint256 percentage) internal {
if (percentage > 5e17) revert PERCENTAGE_VALUE_INVALID(5e17, percentage);
datasetOwnerPercentage = percentage;
}
/**
* @notice Internal function that sets the weights of the respective provided tags.
* @dev Called by `setTagWeights()` and `setDSOwnerPercentageAndTagWeights`
* @param tags The tags participating in the payment distributions
* @param weights The weights of the respective tags to set
*/
function _setTagWeights(bytes32[] calldata tags, uint256[] calldata weights) internal {
EnumerableMap.Bytes32ToUintMap storage tagWeights = _versionedTagWeights.push();
uint256 weightSum;
for (uint256 i; i < weights.length; i++) {
weightSum += weights[i];
tagWeights.set(tags[i], weights[i]);
}
if (weightSum != 1e18) revert TAG_WEIGHTS_SUM_INVALID(1e18, weightSum);
We advise the code to maintain an administrator-controlled implementation whitelist per manager type that the DatasetNFT::setManagers
function sanitizes its inputs against, ensuring that the implementations attached to a particular data-set ID are secure and valid.
Type | Severity | Location |
---|---|---|
Gas Optimization | FragmentNFT.sol:L208 |
The linked for
loop evaluates its limit inefficiently on each iteration.
for (uint256 i; i < tagCount.length(); i++) {
We advise the statements within the for
loop limit to be relocated outside to a local variable declaration that is consequently utilized for the evaluation to significantly reduce the codebase's gas cost. We should note the same optimization is applicable for storage reads present in such limits as they are newly read on each iteration (i.e. length
members of arrays in storage).
Type | Severity | Location |
---|---|---|
Logical Fault | AcceptManuallyVerifier.sol:L28-L30, L36-L38, L73-L75, L77, L91-L93, L95, L108-L110, L114 |
All referenced statements utilize the fragmentNFT
the caller specifies and utilize its getter functions to assess the address at which an IVerifierManager::resolve
call should be performed at.
This evaluation path is insecure as it is possible for a user to submit a malicious fragmentNFT
that passes the relevant access-control checks for the AcceptManuallyVerifier::onlyVerifierManager
/ AcceptManuallyVerifier::onlyDatasetOwner
and ultimately yields a valid IVerifierManager
where the VerifierManager::resolve
call is made to.
As the VerifierManager::resolve
function will evaluate the IFragmentNFT
to perform a call to via the dataset
and datasetId
entries it contains, a malicious contract can be utilized to trick a "manual" acceptance of a fragment ID.
THe AcceptManuallyVerifier
can be completely bypassed by a carefully crafted IFragmentNFT
and IDatasetNFT
compliant malicious contract.
/**
* @notice Resolves a single contribution proposal
* @dev Only callable by the Dataset owner.
* Emits a {FragmentResolved} event.
* @param fragmentNFT The address of the FragmentNFT contract instance
* @param id The ID of the pending Fragment associated with the contribution proposal
* @param accept Flag to indicate acceptance (`true`) or rejection (`true`)
*/
function resolve(address fragmentNFT, uint256 id, bool accept) external onlyDatasetOwner(fragmentNFT) {
VerifierManager vm = VerifierManager(
IDatasetNFT(IFragmentNFT(fragmentNFT).dataset()).verifierManager(IFragmentNFT(fragmentNFT).datasetId())
);
_pendingFragments[fragmentNFT].remove(id);
vm.resolve(id, accept);
emit FragmentResolved(fragmentNFT, id, accept);
}
We advise the code to re-evaluate its approach, configuring the relevant addresses it is meant to invoke in an AcceptManuallyVerifier::constructor
, ensuring that the input fragmentNFT
is validated as a correct implementation by querying the IDatasetNFT::fragmentNFT
getter function with the relevant datasetId
.
Type | Severity | Location |
---|---|---|
Language Specific | VerifierManager.sol:L66-L68, L77-L79, L90 |
The linked functions adjust sensitive contract variables yet do not emit an event for it.
function setDefaultVerifier(address defaultVerifier_) external onlyDatasetOwner {
defaultVerifier = defaultVerifier_;
}
We advise an event
to be declared and correspondingly emitted for each function to ensure off-chain processes can properly react to this system adjustment.
Type | Severity | Location |
---|---|---|
Language Specific | ERC20SubscriptionManager.sol:L91-L96 |
The ERC20SubscriptionManager::_charge
function will not prevent native funds from being sent alongside the call the function is executed in, resulting in the GenericSingleDatasetSubscriptionManager::subscribe
and GenericSingleDatasetSubscriptionManager::extendSubscription
functions locking all native funds that may be accidentally sent when they are invoked.
It is presently possible to lock funds in the ERC20SubscriptionManager
by supplying native funds in either the GenericSingleDatasetSubscriptionManager::subscribe
or GenericSingleDatasetSubscriptionManager::extendSubscription
functions.
function _charge(address subscriber, uint256 amount) internal override {
token.safeTransferFrom(subscriber, address(this), amount);
address distributionManager = dataset.distributionManager(datasetId);
token.approve(distributionManager, amount);
IDistributionManager(distributionManager).receivePayment(address(token), amount);
}
We advise the code to introduce a require
check, ensuring that the msg.value
of the call is 0
to prevent native funds from being accidentally locked in the ERC20SubscriptionManager
.
Type | Severity | Location |
---|---|---|
Language Specific | FragmentNFT.sol:L293-L296 |
The FragmentNFT::proposeMany
function contains a re-entrancy attack vector that manifests when the VerifierManager
calls the FragmentNFT::accept
function in the same call.
Specifically, the FragmentNFT::_proposeManyMessageHash
function will validate a signed payload with a specific _mintCounter
range, however, this range can be invalidated by re-entering the contract during an interim IVerifierManager::propose
call and invoking f.e. DatasetNFT::proposeFragment
, causing the _mintCounter
to be incremented while the loop within FragmentNFT::proposeMany
has not concluded and thus leading to out-of-order tag and pending fragment owner IDs.
The current FragmentNFT::proposeMany
mechanism is insecure as the signed message hash validated in FragmentNFT::_proposeManyMessageHash
can be "invalidated" via a re-entrancy when the IVerifierManager
instantly "accepts" proposals and thus leads to the ERC721::_safeMint
operation being executed within FragmentNFT::accept
which is re-entrant.
function proposeMany(
address[] memory owners,
bytes32[] memory tags_,
bytes calldata signature
) external onlyDatasetNFT {
if (tags_.length != owners.length) revert ARRAY_LENGTH_MISMATCH();
bytes32 msgHash = _proposeManyMessageHash(_mintCounter + 1, _mintCounter + tags_.length, owners, tags_);
address signer = ECDSA.recover(msgHash, signature);
if (!dataset.isSigner(signer)) revert BAD_SIGNATURE(msgHash, signer);
for (uint256 i; i < owners.length; i++) {
uint256 id = ++_mintCounter;
bytes32 tag = tags_[i];
pendingFragmentOwners[id] = owners[i];
tags[id] = tag;
emit FragmentPending(id, tag);
// Here we call VeriferManager and EXPECT it to call `accept()`
// during this call OR at any following transaction.
// DO NOT implement any state changes after this point!
IVerifierManager(dataset.verifierManager(datasetId)).propose(id, tag);
}
}
We advise the code to cache the _mintCounter
to a local cachedMintCounter
variable before the for
loop's execution and to validate it afterwards as being strictly equal to the _mintCounter
(i.e. cachedMintCounter + tags_.length == _mintCounter
).
This mechanism will prevent re-entrancy attacks from affecting the sole storage
related entry utilized within the vulnerable for
loop within FragmentNFT::proposeMany
.
Type | Severity | Location |
---|---|---|
Gas Optimization | GenericSingleDatasetSubscriptionManager.sol:L93 |
The linked for
loop evaluates its limit inefficiently on each iteration.
for (uint256 i; i < subscrs.length(); i++) {
We advise the statements within the for
loop limit to be relocated outside to a local variable declaration that is consequently utilized for the evaluation to significantly reduce the codebase's gas cost. We should note the same optimization is applicable for storage reads present in such limits as they are newly read on each iteration (i.e. length
members of arrays in storage).
Type | Severity | Location |
---|---|---|
Gas Optimization | DistributionManager.sol:L305, L331, L352, L377 |
The linked for
loops evaluate their limit inefficiently on each iteration.
for (uint256 i = firstUnclaimedPayout; i < payments.length; i++) {
We advise the statements within the for
loop limits to be relocated outside to a local variable declaration that is consequently utilized for the evaluations to significantly reduce the codebase's gas cost. We should note the same optimization is applicable for storage reads present in those limits as they are newly read on each iteration (i.e. length
members of arrays in storage).
Type | Severity | Location |
---|---|---|
Gas Optimization | GenericSingleDatasetSubscriptionManager.sol:L93, L348, L368, L394 |
The linked for
loops increment / decrement their iterator "safely" due to Solidity's built - in safe arithmetics (post-0.8.X
).
for (uint256 i; i < subscrs.length(); i++) {
We advise the increment / decrement operations to be performed in an unchecked
code block as the last statement within each for
loop to optimize their execution cost.
Type | Severity | Location |
---|---|---|
Logical Fault | DatasetNFT.sol:L141 |
The DatasetNFT::_mintMessageHash
function will yield a message digest that does not contain the to
member the mint operation will create an NFT to.
As such, all mint operations are susceptible to on-chain race conditions whereby a DatasetNFT::mint
operation is detected and a malicious actor submits a transaction with a higher priority that changes the to
argument to their choosing.
The signature validation mechanism in DatasetNFT::mint
is ineffectual as it does not validate the to
member the NFT is being minted for, permitting anyone to claim any signature.
function mint(bytes32 uuidHashed, address to, bytes calldata signature) external returns (uint256) {
bytes32 msgHash = _mintMessageHash(uuidHashed);
address signer = ECDSA.recover(msgHash, signature);
if (!hasRole(SIGNER_ROLE, signer)) revert BAD_SIGNATURE(msgHash, signer);
uint256 id = uint256(uuidHashed);
_mint(to, id);
return id;
}
We advise the code to properly attach the to
member with the uuidHashed
, preventing on-chain race conditions from being able to "steal" the mint operation of a particular data-set ID.
Type | Severity | Location |
---|---|---|
Language Specific | FragmentNFT.sol:L480, L495, L563 |
The linked mathematical operations are guaranteed to be performed safely by surrounding conditionals evaluated in either require
checks or if-else
constructs.
if (arr.length == 0) arr.push();
return arr[arr.length - 1];
Given that safe arithmetics are toggled on by default in pragma
versions of 0.8.X
, we advise the linked statements to be wrapped in unchecked
code blocks thereby optimizing their execution cost.
Type | Severity | Location |
---|---|---|
Standard Conformity | DistributionManager.sol:L196 |
The DistributionManager::receivePayment
function will greatly misbehave when dealing with tokens that contain a fee-on-transfer mechanism as it will assume to have received the full amount
transferred.
The severity of this exhibit will be adjusted depending on whether the Nexera Protocol team intended to support fee-on-transfer tokens in their DistributionManager
system.
function receivePayment(address token, uint256 amount) external payable onlySubscriptionManager nonReentrant {
if (_versionedTagWeights.length == 0) revert TAG_WEIGHTS_NOT_INITIALIZED();
if (address(token) == address(0)) {
if (amount != msg.value) revert MSG_VALUE_MISMATCH(msg.value, amount);
} else {
IERC20(token).safeTransferFrom(_msgSender(), address(this), amount);
}
uint256 snapshotId = fragmentNFT.snapshot();
// Deployer fee
uint256 deployerFee = (amount * dataset.deployerFeePercentage(datasetId)) / 1e18;
We advise the code to either explicitly specify that fee-on-transfer tokens are not supported or to properly support them by tracking the before-and-after balance measurements of the contract, either of which we consider an adequate resolution to this exhibit.
Type | Severity | Location |
---|---|---|
Code Style | GenericSingleDatasetSubscriptionManager.sol:L114, L272, L300, L310, L311 |
The linked value literals are repeated across the codebase multiple times.
if (durationInDays == 0 || durationInDays > 365) revert SUBSCRIPTION_DURATION_INVALID(1, 365, durationInDays);
We advise each to be set to its dedicated constant
variable instead optimizing the legibility of the codebase.
mapping
LookupsType | Severity | Location |
---|---|---|
Gas Optimization | FragmentNFT.sol:L427, L430, L452, L455 |
The linked statements perform key-based lookup operations on mapping
declarations from storage multiple times for the same key redundantly.
function _updateAccountSnapshot(address account, uint256 firstTokenId, uint256 batchSize, bool add) private {
uint256 currentSnapshot = _currentSnapshotId();
EnumerableMap.Bytes32ToUintMap storage currentAccountTagCount = _snapshots[currentSnapshot].accountTagCount[
account
];
uint256 lastAccountSnapshot = _lastUint256ArrayElement(_accountSnapshotIds[account]);
if (lastAccountSnapshot < currentSnapshot) {
_copy(_snapshots[lastAccountSnapshot].accountTagCount[account], currentAccountTagCount);
_accountSnapshotIds[account].push(currentSnapshot);
}
for (uint256 i; i < batchSize; i++) {
uint256 id = firstTokenId + i;
bytes32 tag = tags[id];
(, uint256 currentCount) = currentAccountTagCount.tryGet(tag);
currentAccountTagCount.set(tag, add ? (currentCount + 1) : (currentCount - 1));
}
}
As the lookups internally perform an expensive keccak256
operation, we advise the lookups to be cached wherever possible to a single local declaration that either holds the value of the mapping
in case of primitive types or holds a storage
pointer to the struct
contained.
mapping
LookupsType | Severity | Location |
---|---|---|
Gas Optimization | DatasetNFT.sol:L162, L163, L166, L167, L170, L171 |
The linked statements perform key-based lookup operations on mapping
declarations from storage multiple times for the same key redundantly.
function setManagers(uint256 id, ManagersConfig calldata config) external onlyTokenOwner(id) {
bool changed;
if (configurations[id].subscriptionManager != config.subscriptionManager) {
proxies[id].subscriptionManager = _cloneAndInitialize(config.subscriptionManager, id);
changed = true;
}
if (configurations[id].distributionManager != config.distributionManager) {
proxies[id].distributionManager = _cloneAndInitialize(config.distributionManager, id);
changed = true;
}
if (configurations[id].verifierManager != config.verifierManager) {
proxies[id].verifierManager = _cloneAndInitialize(config.verifierManager, id);
changed = true;
}
if (changed) {
configurations[id] = config;
emit ManagersConfigChange(id);
}
}
As the lookups internally perform an expensive keccak256
operation, we advise the lookups to be cached wherever possible to a single local declaration that either holds the value of the mapping
in case of primitive types or holds a storage
pointer to the struct
contained.
Type | Severity | Location |
---|---|---|
Gas Optimization | FragmentNFT.sol:L184, L208, L234, L286, L365, L457, L549 |
The linked for
loops increment / decrement their iterator "safely" due to Solidity's built - in safe arithmetics (post-0.8.X
).
for (uint256 i; i < length; i++) {
We advise the increment / decrement operations to be performed in an unchecked
code block as the last statement within each for
loop to optimize their execution cost.
Type | Severity | Location |
---|---|---|
Gas Optimization | DistributionManager.sol:L120, L174, L305, L331, L352, L377, L410 |
The linked for
loops increment / decrement their iterator "safely" due to Solidity's built - in safe arithmetics (post-0.8.X
).
for (uint256 i; i < tagsLength; i++) {
We advise the increment / decrement operations to be performed in an unchecked
code block as the last statement within each for
loop to optimize their execution cost.
Type | Severity | Location |
---|---|---|
Gas Optimization | DatasetFactory.sol:L6-L8 |
The DatasetFactory
contract will contain an inflated bytecode size as it imports the full VerifierManager
, DistributionManager
, and ERC20SubscriptionManager
implementations even though they are solely utilized as interface
declarations.
import {VerifierManager} from "./verifier/VerifierManager.sol";
import {DistributionManager} from "./distribution/DistributionManager.sol";
import {ERC20SubscriptionManager} from "./subscription/ERC20SubscriptionManager.sol";
We advise a proper interface
to be coded for each contract (i.e. IVerifierManager
) and to be imported to the DatasetFactory
, minimizing its deployed bytecode size.
Type | Severity | Location |
---|---|---|
Gas Optimization | DistributionManager.sol:L369 |
The DistributionManager::_claimPayouts
function is solely invoked by the DistributionManager::claimDatasetOwnerAndFragmentPayouts
function which will in turn supply the ContextUpgradeable::_msgSender
as the function's argument at all times.
function _claimPayouts(address beneficiary) internal {
// Claim payouts
uint256 firstUnclaimedPayout = _firstUnclaimedContribution[beneficiary];
if (firstUnclaimedPayout >= payments.length) return; // Nothing to claim
_firstUnclaimedContribution[beneficiary] = payments.length; // CEI pattern to prevent reentrancy
address collectToken = payments[firstUnclaimedPayout].token;
uint256 collectAmount;
for (uint256 i = firstUnclaimedPayout; i < payments.length; i++) {
Payment storage p = payments[i];
if (collectToken != p.token) {
// Payment token changed, send what we've already collected
_sendPayout(collectToken, collectAmount, beneficiary);
collectToken = p.token;
collectAmount = 0;
}
collectAmount += _calculatePayout(p, beneficiary);
}
// send collected and not sent yet
_sendPayout(collectToken, collectAmount, beneficiary);
}
We advise the code to omit its input argument and to utilize the ContextUpgradeable::_msgSender
value directly, optimizing its gas cost.
This will also permit the function to be invoked by the DistributionManager::claimPayouts
function, reducing the overall bytecode size of the contract.
Type | Severity | Location |
---|---|---|
Gas Optimization | AcceptManuallyVerifier.sol:L8 |
The AcceptManuallyVerifier
contract will contain an inflated bytecode size as it imports the full VerifierManager
implementation even though it is solely utilized as an interface
.
import {VerifierManager} from "./VerifierManager.sol";
We advise a proper interface
to be coded for the relevant contract (i.e. IVerifierManager
) and to be imported to the AcceptManuallyVerifier
, minimizing its deployed bytecode size.
Type | Severity | Location |
---|---|---|
Input Sanitization | VerifierManager.sol:L66-L68 |
The linked function accepts an address
argument yet does not properly sanitize it.
The presence of zero-value addresses, especially in constructor
implementations, can cause the contract to be permanently inoperable. These checks are advised as zero-value inputs are a common side-effect of off-chain software related bugs.
function setDefaultVerifier(address defaultVerifier_) external onlyDatasetOwner {
defaultVerifier = defaultVerifier_;
}
We advise some basic sanitization to be put in place by ensuring that the address
specified is non-zero.
Type | Severity | Location |
---|---|---|
Gas Optimization | DatasetNFT.sol:L107, L108 |
The DatasetNFT::_contractURI
functions are invoked twice redundantly in the DatasetNFT::tokenURI
function.
function tokenURI(uint256 tokenId) public view override(ERC721Upgradeable, IDatasetNFT) returns (string memory) {
if (!_exists(tokenId)) revert TOKEN_ID_NOT_EXISTS(tokenId);
string memory contractURI_ = string.concat(_contractURI(), "/");
return bytes(_contractURI()).length > 0 ? string.concat(contractURI_, tokenId.toString()) : "";
}
We advise the function to be invoked once and its result to be stored to a string memory
variable that is consequently re-used in place of its second invocation, optimizing the function's gas cost.
Type | Severity | Location |
---|---|---|
Code Style | DistributionManager.sol:L161, L201, L211 |
The linked value literals are repeated across the codebase multiple times.
if (percentage > 5e17) revert PERCENTAGE_VALUE_INVALID(5e17, percentage);
We advise each to be set to its dedicated constant
variable instead optimizing the legibility of the codebase.
Type | Severity | Location |
---|---|---|
Code Style | DistributionManager.sol:L161 |
The linked representation of a numeric literal is sub-optimally represented decreasing the legibility of the codebase.
if (percentage > 5e17) revert PERCENTAGE_VALUE_INVALID(5e17, percentage);
To properly illustrate the value's purpose, we advise the following guidelines to be followed.
For values meant to depict fractions with a base of 1e18
, we advise fractions to be utilized directly (i.e. 1e17
becomes 0.1e18
) as they are supported.
For values meant to represent a percentage base, we advise each value to utilize the underscore (_
) separator to discern the percentage decimal (i.e. 10000
becomes 100_00
, 300
becomes 3_00
and so on).
Finally, for large numeric values we simply advise the underscore character to be utilized again to represent them (i.e. 1000000
becomes 1_000_000
).
Type | Severity | Location |
---|---|---|
Logical Fault | GenericSingleDatasetSubscriptionManager.sol:L277, L328 |
The GenericSingleDatasetSubscriptionManager::_subscribe
and GenericSingleDatasetSubscriptionManager::_extendSubscription
functions do not permit the caller to control how much funds they are charged for a particular subscription, permitting a malicious subscription manager to arbitrarily charge them.
In detail, the ERC20SubscriptionManager::setFee
function permits the fee to be arbitrarily set by the data-set owner leading to an on-chain race condition whereby a data-set owner detects a subscription and alters the fee of the subscription with a higher-priority transaction.
The vulnerability is trivial to exploit by creating a seemingly lucrative data-set whose price is consequentially altered maliciously the moment a subscription is detected, leading to "loss of funds" for the subscriber.
function _subscribe(uint256 ds, uint256 durationInDays, uint256 consumers) internal returns (uint256 sid) {
_requireCorrectDataset(ds);
if (balanceOf(_msgSender()) != 0) revert CONSUMER_ALREADY_SUBSCRIBED(_msgSender());
if (durationInDays == 0 || durationInDays > 365) revert SUBSCRIPTION_DURATION_INVALID(1, 365, durationInDays);
if (consumers == 0) revert CONSUMER_ZERO();
(, uint256 fee) = _calculateFee(durationInDays, consumers);
_charge(_msgSender(), fee);
sid = ++_mintCounter;
SubscriptionDetails storage sd = _subscriptions[sid];
sd.validSince = block.timestamp;
sd.validTill = block.timestamp + (durationInDays * 1 days);
sd.paidConsumers = consumers;
_safeMint(_msgSender(), sid);
emit SubscriptionPaid(sid, sd.validSince, sd.validTill, sd.paidConsumers);
}
We advise the higher-level GenericSingleDatasetSubscriptionManager::subscribe
and GenericSingleDatasetSubscriptionManager::extendSubscription
functions to accept an additional argument that permits the subscriber to enforce an upper-limit on the amount they are willing to be charged for the particular operation, preventing the aforementioned race condition from manifesting.
Type | Severity | Location |
---|---|---|
Language Specific | GenericSingleDatasetSubscriptionManager.sol:L22-L24 |
The SubscriptionPaid
, ConsumerAdded
, and ConsumerRemoved
events of the GenericSingleDatasetSubscriptionManager
contract do not contain any indexed
arguments.
event SubscriptionPaid(uint256 id, uint256 validSince, uint256 validTill, uint256 paidConsumers);
event ConsumerAdded(uint256 id, address consumer);
event ConsumerRemoved(uint256 id, address consumer);
We advise them to be introduced, particularly in relation to the id
as well as consumer
to ensure off-chain processes can optimally filter these events.
Type | Severity | Location |
---|---|---|
Gas Optimization | FragmentNFT.sol:L427, L452, L561-L564 |
In the current structure of the FragmentNFT
, the _accountSnapshotIds
will always possess an empty 0
entry as its first element which would indicate that the first snapshot ID is the "last" snapshot of all accounts incorrectly.
function _updateAccountSnapshot(address account, uint256 firstTokenId, uint256 batchSize, bool add) private {
uint256 currentSnapshot = _currentSnapshotId();
EnumerableMap.Bytes32ToUintMap storage currentAccountTagCount = _snapshots[currentSnapshot].accountTagCount[
account
];
uint256 lastAccountSnapshot = _lastUint256ArrayElement(_accountSnapshotIds[account]);
if (lastAccountSnapshot < currentSnapshot) {
_copy(_snapshots[lastAccountSnapshot].accountTagCount[account], currentAccountTagCount);
_accountSnapshotIds[account].push(currentSnapshot);
}
for (uint256 i; i < batchSize; i++) {
uint256 id = firstTokenId + i;
bytes32 tag = tags[id];
(, uint256 currentCount) = currentAccountTagCount.tryGet(tag);
currentAccountTagCount.set(tag, add ? (currentCount + 1) : (currentCount - 1));
}
}
We advise the code to simply initialize the _snapshots
entry with two empty snapshots rather than one in FragmentNFT::initialize
.
This will ensure that the valid FragmentNFT::_currentSnapshotId
values start from 1
rather than 0
, allowing the FragmentNFT::_lastUint256ArrayElement
function to immediately yield 0
instead of pushing an empty element to its arr
, significantly optimizing the account snapshot system.
Type | Severity | Location |
---|---|---|
Logical Fault | DistributionManager.sol:L91 |
The DistributionManager::initialize
function will incorrectly initialize the distribution manager when no fragment NFT has been deployed yet for a particular datasetId
as the DatasetNFT::setManagers
function permits the configuration of a distributionManager
even if no fragment NFT has yet been deployed.
A misconfiguration of the DistributionManager
would be undetected by front-end processes as the DatasetNFT::setManagers
function can be invoked at any time, leading to the vulnerability manifesting when the DistributionManager
attempts to receive funds via the DistributionManager::receivePayment
function.
function initialize(address dataset_, uint256 datasetId_) external initializer {
__ReentrancyGuard_init();
dataset = IDatasetNFT(dataset_);
datasetId = datasetId_;
fragmentNFT = IFragmentNFT(dataset.fragmentNFT(datasetId));
}
We advise the code to properly ensure that the fragmentNFT
entry extracted from the dataset
is non-zero, reverting in any other case to prevent deploying a DistributionManager
with a misconfigured fragmentNFT
.
Type | Severity | Location |
---|---|---|
Gas Optimization | FragmentNFT.sol:L122, L123, L142, L144 |
The FragmentNFT::_contractURI
and FragmentNFT::_baseURI
functions are invoked twice redundantly in the FragmentNFT::tokenURI
and FragmentNFT::_contractURI
functions respectively.
function tokenURI(uint256 tokenId) public view override returns (string memory) {
if (!_exists(tokenId)) revert TOKEN_ID_NOT_EXISTS(tokenId);
string memory contractURI_ = string.concat(_contractURI(), "/");
return bytes(_contractURI()).length > 0 ? string.concat(contractURI_, tokenId.toString()) : "";
}
We advise each function to be invoked once and its result to be stored to a string memory
variable that is consequently re-used in place of its second invocation, optimizing each function's gas cost.
Type | Severity | Location |
---|---|---|
Gas Optimization | VerifierManager.sol:L89 |
The linked for
loop increments / decrements the iterator "safely" due to Solidity's built-in safe arithmetics (post-0.8.X
).
for (uint256 i; i < tags.length; i++) {
We advise the increment / decrement operation to be performed in an unchecked
code block as the last statement within the for
loop to optimize its execution cost.
Type | Severity | Location |
---|---|---|
Input Sanitization | FragmentNFT.sol:L253, L277 |
The FragmentNFT::propose
and FragmentNFT::proposeMany
functions do not validate their perspective address
arguments (to
/ owners
) as being non-zero, potentially leading to proposals that can neither be accepted or rejected.
The current FragmentNFT
system permits unfulfillable proposals to be created and the relevant DatasetNFT
functions (DatasetNFT::proposeFragment
/ DatasetNFT::proposeManyFragments
) do not perform any validation to prevent this.
/**
* @notice Accepts a specific proposed contribution by minting the respective pending Fragment NFT to contributor
* @dev Only callable by VerifierManager contract.
* Emits a {FragmentAccepted} event.
* @param id The ID of the pending Fragment NFT associated with the proposed contribution to be accepted
*/
function accept(uint256 id) external onlyVerifierManager {
address to = pendingFragmentOwners[id];
if (_exists(id) || to == address(0)) revert NOT_PENDING_FRAGMENT(id);
delete pendingFragmentOwners[id];
_safeMint(to, id);
emit FragmentAccepted(id);
}
/**
* @notice Rejects a specific proposed contribution by removing the associated pending Fragment NFT
* @dev Only callable by VerifierManager contract.
* Emits a {FragmentRejected} event.
* @param id The ID of the pending Fragment NFT associated with the proposed contribution to be rejected
*/
function reject(uint256 id) external onlyVerifierManager {
address to = pendingFragmentOwners[id];
if (_exists(id) || to == address(0)) revert NOT_PENDING_FRAGMENT(id);
delete pendingFragmentOwners[id];
delete tags[id];
emit FragmentRejected(id);
}
We advise the code to properly validate the address
arguments as being non-zero, preventing unfulfillable proposals from being created that would need to be manually removed via the FragmentNFT::remove
function available to the data-set owner.
Type | Severity | Location |
---|---|---|
Logical Fault | DatasetNFT.sol:L187-L199, L216-L219 |
The DatasetNFT::setDeployerFeeModelPercentages
function can be invoked even when the contract has no deployerFeeBeneficiary
specified. This can cause significant misbehaviours in the DistributionManager::receivePayment
function as it will fatally fail if the deployerFeeBeneficiary
has not been specified when a non-zero fee is expected to be applied.
While the misconfiguration is trivial to resolve, it should be prohibited at the code level as otherwise valid subscriptions may fail to execute due to the DistributionManager::receivePayment
function's failure, in turn leading to the subscription being lost as the subscriber may not wish to retry their operation.
/**
* @notice Sets the fee percentages for the provided Deployer Fee Models
* @dev Only callable by DatasetNFT ADMIN.
* Percentages are encoded such that 100% is represented as 1e18.
* @param models An array of Deployer Fee Models to set percentages for (see `IDatasetNFT.sol`)
* @param percentages An array of corresponding fee percentages
*/
function setDeployerFeeModelPercentages(
DeployerFeeModel[] calldata models,
uint256[] calldata percentages
) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (models.length != percentages.length) revert ARRAY_LENGTH_MISMATCH();
for (uint256 i; i < models.length; i++) {
DeployerFeeModel m = models[i];
if (uint8(m) == 0) revert INVALID_ZERO_MODEL_FEE();
uint256 p = percentages[i];
if (p > 1e18) revert PERCENTAGE_VALUE_INVALID(1e18, p);
deployerFeeModelPercentage[m] = p;
}
}
/**
* @notice Sets the deployer fee model for a specific Dataset
* @dev Only callable by DatasetNFT ADMIN
* @param datasetId The ID of the target Dataset NFT token
* @param model The Deployer Fee Model to set
*/
function setDeployerFeeModel(uint256 datasetId, DeployerFeeModel model) external onlyRole(DEFAULT_ADMIN_ROLE) {
deployerFeeModels[datasetId] = model;
}
/**
* @notice Sets the address of the deployer fee beneficiary
* @dev Only callable by DatasetNFT ADMIN
* @param deployerFeeBeneficiary_ The address to set as the beneficiary
*/
function setDeployerFeeBeneficiary(address deployerFeeBeneficiary_) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (deployerFeeBeneficiary_ == address(0)) revert ZERO_ADDRESS();
deployerFeeBeneficiary = deployerFeeBeneficiary_;
}
We advise the DatasetNFT::setDeployerFeeModelPercentages
function to ensure that the deployerFeeBeneficiary
has been specified to a non-zero value when it is invoked, preventing a non-zero fee from being applied to a particular model and data-set ID when no beneficiary for it has been specified.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.