Download audit report
This document presents the security review performed by Nethermind Security for the smart contracts of WilderWorld. The audit scope comprises of smart contracts related to Staking, Voting, and DAO smart contracts.
The staking contracts allow users to stake ERC20 or ERC721 tokens in locked or unlocked mode. In locked mode, the user's tokens are held for a fixed period, earning a higher yield. Conversely, unlocked mode offers more flexibility, allowing users to withdraw their funds at any time.
Upon staking, the protocol mints stake representation tokens, which serve as voting tokens for participation in ZDAO governance. The ZDAO governance is built based on OpenZeppelin’s governance contracts.
The audited code comprises 1093 lines of code written in the Solidity language. This security review focuses exclusively on the smart contracts listed in Section 2 (Audited Files).
The audit was performed using (a) manual analysis of the codebase and (b) creation of test cases.
| Audit assessment | |
| Audit Type | Security Review |
| Initial Report | Jan 20, 2025 |
| Response from Client | Regular responses during audit engagement |
| Final Report | Mar 13, 2025 |
| Repository | zModules |
| Commit (Audit) | 97e93a1979 |
| Commit (Final) | f7a82d2853 |
| Documentation Assessment | Medium |
| Test Suite Assessment | Medium |
Findings by severity
| # | Contract | LoC | Comments | Ratio | Blank | Total |
| 1 | voting/ZeroVotingERC20.sol | 57 | 26 | 45.6% | 10 | 93 |
| 2 | voting/ZeroVotingERC721.sol | 111 | 36 | 32.4% | 26 | 173 |
| 3 | voting/IZeroVotingERC20.sol | 22 | 1 | 4.5% | 6 | 29 |
| 4 | voting/IZeroVotingERC721.sol | 26 | 5 | 19.2% | 13 | 44 |
| 5 | dao/ZDAO.sol | 189 | 48 | 25.4% | 15 | 252 |
| 6 | staking/StakingBase.sol | 198 | 154 | 77.8% | 55 | 407 |
| 7 | staking/IStakingBase.sol | 84 | 101 | 120.2% | 37 | 222 |
| 8 | staking/ERC20/IStakingERC20.sol | 21 | 22 | 104.8% | 12 | 55 |
| 9 | staking/ERC20/StakingERC20.sol | 149 | 85 | 57.0% | 44 | 278 |
| 10 | staking/ERC721/StakingERC721.sol | 199 | 97 | 48.7% | 50 | 346 |
| 11 | staking/ERC721/IStakingERC721.sol | 37 | 33 | 89.2% | 17 | 87 |
| Total | 1093 | 608 | 55.6% | 285 | 1986 |
WilderWorld consists of a collection of modular smart contracts, organized into three core functional areas: Staking, Voting tokens and a DAO module. The following sections provide a detailed explanation of the interactions within these modules.

The staking module enables users to stake either ERC20 or ERC721 tokens through the StakingERC20 and StakingERC721 contracts, respectively. Upon staking, users receive a representative token that allows them to participate in the protocol's governance.
4.1.1 Staking via StakingERC20
Users can stake ERC20 or native tokens into the StakingERC20 contract. Upon staking, an equivalent amount of a representative token is minted for the user. Staking can occur in two modes:
stakeWithLock(...) function.function stakeWithLock(uint256 amount, uint256 lockDuration) external payable overridestakeWithoutLock(...) function.function stakeWithoutLock(uint256 amount) external payable override4.1.2 Staking via StakingERC721
The StakingERC721 contract facilitates the staking of ERC721 tokens. Similar to ERC20 staking, users can stake with or without a lock. Upon staking an ERC721 token, a representative ERC721 token with the same tokenId is minted for the user.
stakeWithLock(...) function allows ERC721 staking with a lock.function stakeWithLock(uint256[] calldata tokenIds, string[] calldata tokenUris, uint256 lockDuration) external override stakeWithoutLock(...) function allows ERC721 staking without a lock.function stakeWithoutLock(uint256[] calldata tokenIds, string[] calldata tokenUris) external override 4.1.3 Claiming Rewards
Rewards are calculated based on the amount staked, the staking duration, and the staking mode. The reward formula is:
rewards = rewardsMultiplier * amount * config.rewardsPerPeriod * timeDuration / config.periodLength / divisorThe accrued rewards are tracked in the contracts based on the mode of staking. While the rewards for unlocked mode are claimable at any time, the rewards for locked mode will become available only after the lock period expires. Users can claim their accrued rewards via the claim(...) function.
function claim() external override4.1.4 Unstaking
Users can unstake their tokens by burning the representative token and receiving the staked amount. Rewards are calculated and transferred accordingly.
In the case of locked tokens, unstaking is only allowed after the lock period. However, if users wish to exit early, they can set the exit flag to true, which will force unstaking but invalidate accrued rewards. This behavior is consistent across both ERC20 and ERC721 staking. Note that in the latest code, the exit functionality has been implemented in a separate exit(...) function.
function exit(bool locked) external override nonReentrant
function exit(uint256[] memory _tokenIds, bool _locked) public override nonReentrantFor the StakingERC20 contract, users can unstake non-locked funds using the unstake(...) function, renamed into unstakeUnlocked(...) in the latest version of the code.
function unstakeUnlocked(uint256 amount) external overrideFor locked funds, the unstakeLocked(...) function is used. This function reverts if funds are still within their lock period.
function unstakeLocked(uint256 amount) external payable overrideFor the StakingERC721 contract, both locked and unlocked tokens can be unstaked using the unstake(...) function. This function was split in the latest code into two different functions similar to the StakingERC20 contract:
function unstakeLocked(uint256[] memory _tokenIds) public override nonReentrant
function unstakeUnlocked(uint256[] memory _tokenIds) public override nonReentrantThe protocol supports voting for both ERC20 and ERC721 tokens. Users can delegate their voting rights to others, and voting tokens are non-transferable. The following contracts facilitate the voting functionality:
ZeroVotingERC20 contract implements an ERC20 voting token.ZeroVotingERC721 contract implements an ERC721 voting token.The protocol’s ZDAO contract, built using OpenZeppelin governance modules, enables eligible users to create proposals, vote on them, and delegate votes to others. Proposals that meet the required threshold are executed and incorporated into the protocol’s operations.
The risk rating methodology used by Nethermind Security follows the principles established by the OWASP Foundation. The severity of each finding is determined by two factors: Likelihood and Impact.
Likelihood measures how likely the finding is to be uncovered and exploited by an attacker. This factor will be one of the following values:
When defining the likelihood of a finding, other factors are also considered. These can include but are not limited to motive, opportunity, exploit accessibility, ease of discovery, and ease of exploit.
Impact is a measure of the damage that may be caused if an attacker exploits the finding. This factor will be one of the following values:
When defining the impact of a finding, other factors are also considered. These can include but are not limited to data and state integrity, loss of availability, financial loss, and reputation damage. After defining the likelihood and impact of an issue, the severity is determined according to the table below.
| Impact / Likelihood | Low | Medium | High |
| High | Medium | High | Critical |
| Medium | Low | Medium | High |
| Low | Info / Best Practices | Low | Medium |
| Undetermined | Undetermined | Undetermined | Undetermined |
To address issues that do not fit a High, Medium or Low severity, Nethermind Security also uses three more finding severities: Informational, Best Practices, and Undetermined.
| File | Fix commit | Type | Status |
StakingERC721.sol | e327e43 | Critical | Fixed |
In the current implementation, when a user initiates an unstake operation with the exit flag set to true for a token that is still locked, the system behaves as expected: it unstakes the token but does not transfer rewards to the user. However, it fails to properly reset the owedRewardsLocked variable, which holds the pre-computed reward amount for the entire lock duration.
function _unstakeMany(...) internal {
// ...
for (i; i < _tokenIds.length;){
// ...
if (nftStaker.locked[_tokenIds[i]]) {
// ...
if (_getRemainingLockTime(nftStaker.stake) == 0) {
// ...
nftStaker.stake.owedRewardsLocked = 0; // @audit reset only when claimed
rewardsGivenLocked = true;
}
// ...
_unstake(_tokenIds[i]);
// ...
}
// ...
}
// ...
if (!exit) {
// Transfer the user's rewards
_transferAmount(config.rewardsToken, rewards);
emit Claimed(msg.sender, rewards);
}
}As a result, the user can claim these rewards via the claim() function in the case of a partial unstake. Alternatively, the user can make another stake to access these rewards. This allows malicious users to exploit the system by creating stakes with long lock durations, exiting the stake early, and then claiming pre-computed rewards, potentially depleting the contract's funds.
Recommendation
The owedRewardsLocked value should be reset when unstaking locked funds.
| File | Fix commit | Type | Status |
StakingBase.sol | e7ec0da | High | Fixed |
In the _coreStake(...) function, when a user stakes a new amount with a locking period while their previous lock has not yet expired, the new stake is only locked for the remaining time of the previous lock. However, the rewards for the new stake are still calculated using the previous lock's rewardsMultiplier, which was intended for the entire previous lock duration. This results in the new stake receiving a disproportionately high rewards multiplier based on the full original lock period, even though it is only locked for the remaining time.
function _coreStake(...) internal {
// ...
if (block.timestamp > staker.unlockedTimestamp) {
// ...
} else {
staker.owedRewardsLocked += _getStakeRewards(
amount,
staker.rewardsMultiplier, //@audit multiplier for the previous lock duration
_getRemainingLockTime(staker),
true);
}
}This issue can be exploited by users who stake large amounts near the end of their previous lock period. They can benefit from a higher rewards multiplier based on the full lock duration despite their new stake being locked for a much shorter period.
Recommendation
The rewardsMultiplier should be recomputed to reflect the remaining lock time for the new stake rather than relying on the multiplier from the previous lock.
| File | Fix commit | Type | Status |
StakingBase.sol | dd2a9ec | High | Fixed |
The owner of the staking contracts has the ability to modify configuration values that impact reward calculations, notably:
uint256 rewardsPerPeriod
uint256 periodLength
uint256 minimumRewardsMultiplier
uint256 maximumRewardsMultiplierHowever, in the current design, changes to these parameters impact rewards computation for historical stakes and not the newest ones only.
For example, if a user stakes tokens and the owner later changes the reward parameters during the staking period ( updates the rewardsPerPeriod, for example), the system currently recalculates all previous rewards using the new configuration. This leads to a jump in accrued rewards, where rewards from earlier periods are either over- or under-calculated, depending on the direction of the change.
function _getStakeRewards(...) internal view returns (uint256) {
uint256 divisor = locked ? LOCKED_PRECISION_DIVISOR : PRECISION_DIVISOR;
return rewardsMultiplier * amount * config.rewardsPerPeriod * timeDuration / config.periodLength / divisor;
}Recommendation
Ensure that changes to configuration values should only apply to rewards from the time they are updated and should not affect rewards for past periods. Alternatively, all user rewards must be accounted for and stored before performing any parameter update.
| File | Fix commit | Type | Status |
StakingERC20.solStakingERC721.sol | 96191f2 | High | Fixed |
When a user unstakes a portion of their tokens, the contract computes rewards based on the withdrawn amount rather than the total amount staked. Refer to the code snippet below, where accrued rewards are being computed using the amount withdrawn.
function _unstake(uint256 amount, bool locked, bool exit) internal {
// ...
// most recent timestamp?
rewards = staker.owedRewards + _getStakeRewards(
amount, //@audit only rewards for unstaked amount are taken
1, // Rewards multiplier
block.timestamp - staker.lastTimestamp,
false
);
if (staker.amountStaked == amount) {
...
} else {
staker.amountStaked -= amount;
staker.lastTimestamp = block.timestamp;
}
// ...
}After the partial unstake, the contract updates the staker.lastTimestamp to the current block.timestamp, considering that the entire staking period rewards are accounted for. Consequently, users lose the accrued rewards for the remaining staked amount.
Note that the present issue applies to both the ERC20 and ERC721 staking contracts, including both locked and non-locked tokens.
Recommendation
Use the total staked amount instead of the withdrawn amount when calculating the accrued rewards. For ERC20Staking, the staker.amountStaked can be used, while in ERC721Staking, the number of staked tokens must be accounted for.
_unstakeMany(...)| File | Fix commit | Type | Status |
StakingERC721.sol | b18e44f | Medium | Fixed |
The staking process is designed to enforce that a user must perform a full exit when unstaking. In the StakingERC721 case, this is enforced by the following check in the _unstakeMany(...) function.
if (exit && _tokenIds.length < nftStaker.stake.amountStakedLocked) {
revert NotFullExit();
}However, the function currently allows users to submit arbitrary or invalid token IDs. These invalid IDs are skipped during processing, but they are still counted in the array length. As a result, users can manipulate the array length to bypass the full exit check and perform a partial exit.
if (
nftStaker.staked[_tokenIds[i]] == false
|| IERC721(config.stakeRepToken).ownerOf(_tokenIds[i]) == address(0)
|| IERC721(config.stakeRepToken).ownerOf(_tokenIds[i]) != msg.sender
) {
// Either the list of tokenIds contains a non-existent token
// or it contains a token the owner doesnt own
unchecked {
++i;
}
continue;
}Recommendation
Update the function logic to avoid relying on the array length, as it can be manipulated.
| File | Fix commit | Type | Status |
StakingBase.sol | f9201fd | Medium | Fixed |
Currently, when an existing user stakes additional tokens in locked mode and the previous lock duration has expired, the system automatically includes the previously unlocked amount as part of the new staked and locked amount.
From the user's perspective, the initially locked tokens are unlocked, and their rewards become claimable. However, if the user stakes additional tokens, the previously unlocked tokens are re-locked for a new period, alongside the new tokens. As a result, the previously accumulated rewards are also locked again. If the user attempts to exit during this new lock period, they risk losing the rewards earned during the previous lock period, even though the original lock was successfully completed.
function _coreStake(
Staker storage staker,
uint256 amount,
uint256 lockDuration
) internal {
if (lockDuration == 0) {
// ...
} else {
if (block.timestamp > staker.unlockedTimestamp) {
// The user has never locked or they have and we are past their lock period
// get the user's owed rewards from period in between `unlockedTimestamp` and present at rate of 1
uint256 mostRecentTimestamp = _mostRecentTimestamp(staker);
// this will return 0 if `amountStakedLocked` == 0
staker.owedRewardsLocked += _getStakeRewards(
staker.amountStakedLocked,
1,
block.timestamp - mostRecentTimestamp,
false
);
// Then we update appropriately
staker.unlockedTimestamp = block.timestamp + lockDuration;
staker.rewardsMultiplier = _calcRewardsMultiplier(lockDuration);
// We precalculate the amount because we know the time frame
staker.owedRewardsLocked += _getStakeRewards(
amount,
staker.rewardsMultiplier,
lockDuration,
true
);
} else {...}
staker.lastTimestampLocked = block.timestamp;
//@audit the cumulative amount is locked
staker.amountStakedLocked += amount;
}
}Recommendation
Consider allowing users to unstake and claim their locked rewards once the timelock has expired. Alternatively, document the current process clearly to avoid user mistakes that could result in the locking or loss of accrued rewards.
| File | Fix commit | Type | Status |
StakingERC721.sol | 53967b6 | Low | Fixed |
The nftStaker.locked field is set during staking and stores whether a token Id is locked or not.
During the unstaking process of a locked token, this field is not reset to false.
function _unstakeMany(...) internal {
// ...
for (i; i < _tokenIds.length;){
// ...
if (nftStaker.locked[_tokenIds[i]]) {
// ...
_unstake(_tokenIds[i]);
--nftStaker.stake.amountStakedLocked;
nftStaker.staked[_tokenIds[i]] = false;
isAction = true;
}
// ...
}
// ...
}As a result, if the same token is restaked without a lock, the stale locked entry remains true. This creates an inconsistency where the token is marked as locked in the system, but no rewards are actually computed for the locked stake.
Recommendation
Ensure that the nftStaker.locked flag is properly reset to false after a token is unstaked.
| File | Fix commit | Type | Status |
StakingBase.sol | 889de33 | Info | Fixed |
The StakingBase contract inherits from OpenZeppelin's ReentrancyGuard to prevent reentrancy vulnerabilities. However, for this protection to work, functions within the StakingBase contract and its derived contracts must be explicitly marked with the nonReentrant modifier.
Currently, the nonReentrant modifier has not been applied to any functions in the StakingBase contract nor in its derived contracts (StakingERC20 and StakingERC721).
Recommendation
Assign the nonReentrant modifier to the necessary staking functions.
| File | Fix commit | Type | Status |
StakingBase.sol | e9474a1491a40a | Info | Fixed |
In the current implementation, both the receive() and fallback() functions are defined, but both have empty logic. The receive() function is designed to handle native token transfers without data, while the fallback() function can handle transfers with or without data.
Since both functions serve the same purpose — allowing the contract to receive native tokens — and neither contains any additional logic, only one function is necessary.
Recommendation
Remove the redundant fallback() function.
| File | Fix commit | Type | Status |
StakingBase.sol | 5436a2a | Info | Fixed |
The locked rewards are calculated based on reward multipliers and the duration for which the tokens are locked. One of the factors for lock rewards calculation is the difference between maximumRewardsMultiplier and minimumRewardsMultiplier configured by the owner of the staking contracts.
function _calcRewardsMultiplier(uint256 lock) internal view returns (uint256) {
return config.minimumRewardsMultiplier
+ (config.maximumRewardsMultiplier - config.minimumRewardsMultiplier)
* (lock)
/ config.periodLength;
}However, the setter functions for these parameters lack validation to ensure that maximumRewardsMultiplier is already greater than minimumRewardsMultiplier. Setting invalid values will prevent users from staking tokens for a locked duration. That is because when the _calcRewardsMultiplier(...) function is called during staking, an underflow exception will occur.
Likewise, the function reverts when the periodLength is set to 0.
Recommendation
Add validations in setMinimumRewardsMultiplier and setMaximumRewardsMultiplier setter functions to enforce that maximumRewardsMultiplier is always greater than minimumRewardsMultiplier.
Additionally, ensure that periodLength is always greater than 0 to avoid potential reverts.
ZeroVotingERC721| File | Fix commit | Type | Status |
ZeroVotingERC721.sol | 5f0a8b9 | Info | Fixed |
The total supply of tokens in ZeroVotingERC721 contract is tracked using the _totalSupply state variable. The total supply should reflect the number of tokens in circulation after excluding the tokens that were burnt.
The current implementation of ZeroVotingERC721 contract increments the _totalSupply state variable when minting the new tokens through mint(...) and safeMint(...) functions, but it is not decremented when tokens are burnt through burn(...) function.
Therefore, _totalSupply does not correctly reflect the number of tokens currently in circulation.
Recommendation
The _totalSupply state variable should be decremented when a token is burnt to reflect the total number of tokens in circulation correctly.
payable operator for unstaking functions| File | Fix commit | Type | Status |
StakingERC20.sol | 8bd9636 | Info | Fixed |
The payable operator is unnecessary for unstaking functions in StakingERC20 contract. These functions do not require any native token payment.
function unstake(uint256 amount, bool exit) external payable override {
_unstake(amount, false, exit);
}
function unstakeLocked(uint256 amount, bool exit) external payable override {
_unstake(amount, true, exit);
}Recommendation
Remove the payable operator from the above functions.
| File | Fix commit | Type | Status |
StakingERC20.solStakingERC721.sol | — | Info | Acknowledged |
The staking contract allows users to stake tokens in both locked and unlocked modes. In locked mode, users earn higher rewards, but the staked tokens cannot be withdrawn before the end of the lock period. If a user attempts to exit from a locked stake, they forfeit all rewards. However, in unlocked mode, users have the flexibility to withdraw their stake at any time without forfeiting their rewards.
The current implementation of the StakingERC20 contract incorrectly implements this logic. When unstaking, if the user sets the exit flag to true, rewards are lost even in non-locked mode.
function _unstake(uint256 amount, bool locked, bool exit) internal {
// ...
if (locked) {...}
else { // @audit non-locked mode
if (amount > staker.amountStaked) {
revert UnstakeMoreThanStake();
}
if (exit) {
rewards = 0; //@audit rewards are removed
staker.owedRewards;
} else {...}
// ...
}
// ...
}This issue is also present in the unstaking of ERC721 tokens.
Recommendation
Revise the logic to stop forfeiting rewards in unlocked mode. The exit flag should be applicable only to the locked mode of staking.
Update from Nethermind Security
The Wilderworld team confirmed during a call that this behavior is intentional. The exit flow has been restructured as a separate function, allowing users to withdraw only their initially staked tokens. This new functionality is particularly useful in cases where the contract is unable to provide reward tokens.
ERC20Permit implementation in ZeroVotingERC20| File | Fix commit | Type | Status |
ZeroVotingERC20.sol | 7f7c8bf | Info | Fixed |
The ZeroVotingERC20 contract inherits from the ERC20Permit contract but restricts token transfers through the _update() function, which is intended to prevent transfers of tokens. The ERC20Permit implementation introduces the permit(...) function that allows users to approve token transfers using signed messages. However, since the ZeroVotingERC20 contract explicitly disables token transfers, the permit(...) function becomes non-functional and, therefore, inheriting from the ERC20Permit contract is redundant.
function _update(
address from,
address to,
uint256 value
) internal override(ERC20, ERC20Votes) {
if (from != address(0) && to != address(0)) {
revert NonTransferrableToken();
}
super._update(from, to, value);
}Recommendation
Remove the unused inheritance of ERC20Permit from the ZeroVotingERC20 contract.
transfer() to move native tokens| File | Fix commit | Type | Status |
StakingBase.sol | 2b8c8a5 | Info | Fixed |
In order to transfer native tokens, the StakingBase contract currently uses the transfer function. This is not a recommended function to move native tokens due to a fixed gas limit of 2300. As the gas cost is subject to change as part of infra upgrades.
Additionally, when the recipient is a smart contract like a multi-sig wallet, 2300 gas might not cover the gas required to complete the transfer, leading to a revert.
Recommendation
Use a low-level .call function to move native tokens. Additionally, the reentrancy guard should be implemented in different functionalities to prevent reentrancy attacks on both ERC20Staking and ERC721Staking contracts.
msg.value should be zero when staking ERC20 tokens| File | Fix commit | Type | Status |
StakingERC20.sol | 320f333 | Info | Fixed |
In the StakingERC20 contract, the staking token can either be an ERC20 token or the native gas token.
For gas tokens, funds are received through the payable operator of the stake(...) function. However, when the staking token is an ERC20 token, the function does not enforce msg.value to be zero. As a result, users may inadvertently send msg.value alongside the ERC20 token amount, potentially leading to the loss of funds.
function _stake(uint256 amount, uint256 lockDuration) internal {
if (amount == 0) {
revert ZeroValue();
}
// @audit msg.value must be 0 when stakingToken !=address(0)
if (config.stakingToken == address(0)) {
if (msg.value != amount) {
revert InsufficientValue();
}
}
Staker storage staker = stakers[msg.sender];Recommendation
Add a check to ensure msg.value is 0 when config.stakingToken != address(0)
| File | Fix commit | Type | Status |
IStakingBase.sol | 152b9e7 | Best Practices | Fixed |
The IStakingBase interface defines the following errors, but they are not used in any of the implementation contracts:
error InvalidStake()
error CannotClaim()Recommendation
Remove the unused errors declaration.
| File | Fix commit | Type | Status |
ZeroVotingERC20.sol | 80e57e7 | Best Practices | Fixed |
The ZeroVotingERC20 contract's constructor takes the admin address as an input. This address is granted the DEFAULT_ADMIN_ROLE, BURNER_ROLE, and MINTER_ROLE roles within the contract. However, the function does not check that this address is not set to address(0).
constructor(
string memory name,
string memory symbol,
address admin
) ERC20(name, symbol) ERC20Permit(name){
//@audit Missing zero address check on admin
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(BURNER_ROLE, admin);
_grantRole(MINTER_ROLE, admin);
}Recommendation
Implement a check in the constructor, ensuring admin cannot be set to address(0).
_unstakeMany| File | Fix commit | Type | Status |
StakingERC721.sol | 5d72252 | Best Practices | Fixed |
The _unstakeMany(...) function processes an array of token IDs to be unstaked. For each token, the function checks whether the caller owns it and whether it was previously staked in the contract. This is done through the following checks:
if (
nftStaker.staked[_tokenIds[i]] == false
|| IERC721(config.stakeRepToken).ownerOf(_tokenIds[i]) == address(0) //@audit Unnecessary check
|| IERC721(config.stakeRepToken).ownerOf(_tokenIds[i]) != msg.sender
) {
// Either the list of tokenIds contains a non-existent token
// or it contains a token the owner doesnt own
unchecked {
++i;
}
continue;
}However, the second check is redundant. Specifically, verifying that the token's owner is not address(0) is unnecessary, as the subsequent check already covers this condition, ensuring the token owner is msg.sender.
Recommendation
Remove the redundant check from the code above.
_getContractRewardsBalance function| File | Fix commit | Type | Status |
StakingERC20.sol | 06d92d6 | Best Practices | Fixed |
The StakingERC20 contract inherits from StakingBase and overrides the _getContractRewardsBalance() function to account for staked tokens when calculating the total rewards in the contract. However, the first part of the function duplicates logic from the base contract’s overridden function.
To eliminate redundancy, this code can be replaced by calling super._getContractRewardsBalance() instead.
function _getContractRewardsBalance() internal view override returns (uint256) {
uint256 balance;
//@audit Following if-else block can be replaced by a call to `super._getContractRewardsBalance()`
if (address(config.rewardsToken) == address(0)) {
balance = address(this).balance;
} else {
balance = IERC20(config.rewardsToken).balanceOf(address(this));
}
if (config.rewardsToken == config.stakingToken) {
return balance - totalStaked;
}
return balance;
}Recommendation
Replace the redundant code in _getContractRewardsBalance() with a call to super._getContractRewardsBalance().
stakers mapping in StakingERC721 contract| File | Fix commit | Type | Status |
StakingBase.solStakingERC721.sol | 0310dcd | Best Practices | Fixed |
The StakingBase contract implements common functionalities for staking both ERC20 and ERC721 tokens. It includes the stakers mapping, which is used to track each staker's data via the Staker struct.
contract StakingBase is Ownable, ReentrancyGuard, IStakingBase {
...
/**
* @notice Mapping of each staker to that staker's data in the `Staker` struct
*/
mapping(address user => Staker staker) public stakers;
...
}However, the stakers mapping is only used in the StakingERC20 contract. Since the mapping is declared in the base contract, it becomes part of the storage layout for the StakingERC721 contract as well, which results in redundancy and unnecessary storage usage.
Recommendation
Consider moving the stakers mapping to StakingERC20 interface and implementation contract.
Software documentation refers to the written or visual information that describes the functionality, architecture, design, and implementation of software. It provides a comprehensive overview of the software system and helps users, developers, and stakeholders understand how the software works, how to use it, and how to maintain it. Software documentation can take different forms, such as user manuals, system manuals, technical specifications, requirements documents, design documents, and code comments. Software documentation is critical in software development, enabling effective communication between developers, testers, users, and other stakeholders. It helps to ensure that everyone involved in the development process has a shared understanding of the software system and its functionality. Moreover, software documentation can improve software maintenance by providing a clear and complete understanding of the software system, making it easier for developers to maintain, modify, and update the software over time. Smart contracts can use various types of software documentation. Some of the most common types include:
These types of documentation are essential for smart contract development and maintenance. They help ensure that the contract is properly designed, implemented, and tested, and they provide a reference for developers who need to modify or maintain the contract in the future.
The Wilderworld team was actively present in regular calls, effectively addressing concerns and questions raised by the Nethermind Security team. Additionally, the code includes NatSpec documentation for different functions and their parameters. However, the project documentation could be improved by providing a more comprehensive written overview of the system and an explanation of the different design choices.
All the relevant issues raised by Slither have been incorporated into the issues described in this report.
All the relevant issues raised by the AuditAgent have been incorporated into this report. The AuditAgent is an AI-powered smart contract auditing tool that analyses code, detects vulnerabilities, and provides actionable fixes. It accelerates the security analysis process, complementing human expertise with advanced AI models to deliver efficient and comprehensive smart contract audits. Available at https://app.auditagent.nethermind.io.
| 223Tests passed | 0Failed |
The complete test output is in the full report.
As auditors, we recommend that any changes or updates made to the audited codebase undergo a re-audit or security review to address potential vulnerabilities or risks introduced by the modifications. By conducting a re-audit or security review of the modified codebase, you can significantly enhance the overall security of your system and reduce the likelihood of exploitation. However, we do not possess the authority or right to impose obligations or restrictions on our clients regarding codebase updates, modifications, or subsequent audits. Accordingly, the decision to seek a re-audit or security review lies solely with you.
This report is based on the scope of materials and documentation provided by you to Nethermind in order that Nethermind could conduct the security review outlined in 1. Executive Summary and 2. Audited Files. The results set out in this report may not be complete nor inclusive of all vulnerabilities. Nethermind has provided the review and this report on an as-is, where-is, and as-available basis. You agree that your access and/or use, including but not limited to any associated services, products, protocols, platforms, content, and materials, will be at your sole risk. Blockchain technology remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond the programming language, or other programming aspects that could present security risks. This report does not indicate the endorsement of any particular project or team, nor guarantee its security. No third party should rely on this report in any way, including for the purpose of making any decisions to buy or sell a product, service or any other asset. To the fullest extent permitted by law, Nethermind disclaims any liability in connection with this report, its content, and any related services and products and your use thereof, including, without limitation, the implied warranties of merchantability, fitness for a particular purpose, and non-infringement. Nethermind does not warrant, endorse, guarantee, or assume responsibility for any product or service advertised or offered by a third party through the product, any open source or third-party software, code, libraries, materials, or information linked to, called by, referenced by or accessible through the report, its content, and the related services and products, any hyperlinked websites, any websites or mobile applications appearing on any advertising, and Nethermind will not be a party to or in any way be responsible for monitoring any transaction between you and any third-party providers of products or services. As with the purchase or use of a product or service through any medium or in any environment, you should use your best judgment and exercise caution where appropriate. FOR AVOIDANCE OF DOUBT, THE REPORT, ITS CONTENT, ACCESS, AND/OR USAGE THEREOF, INCLUDING ANY ASSOCIATED SERVICES OR MATERIALS, SHALL NOT BE CONSIDERED OR RELIED UPON AS ANY FORM OF FINANCIAL, INVESTMENT, TAX, LEGAL, REGULATORY, OR OTHER ADVICE.
The Leading Engineers of Blockchain Infrastructure