# Strainz V2
[](https://hackmd.io/q5oSqnU7S_-T2KaRG99cog)
## Version Notes
This is for audit versions?
| Version | No. Pages | Date | Revised By | Notes |
| ------- | ---------- | ---------- | ---------- | ----- |
| 1.0 | ... | 2021-04-22 | ... | |
## Audit notes
**Audit Date:** 2021-04-22
**Auditor(s):** ...
**Auditor Contact Information:** ...
**Notes**: ....
**Audit Report Number:** ...
## Project Information
**Project Name:** Strainz v2
**Description:** Strainz v2 is an upgrade to the existing NFT based farming which adds several new mechanics, a new token and a new NFT.
**Contact:** ...
**Contact Information:** ...
**Token Name(s):** Strainz, Seedz
**Token Symbols:** STRAINZ, SEEDZ
**Contract(s):** N/A
**Code Type:** Smart Contract
**Code Language:** Solidity
**Chain:** Binance Smart Chain
## Executive Summary
## Summary Table
| Severity | Found | Mitigated | Still Open |
| - | - | - | - |
| High Risk | 3 |
| Medium Risk | 0 |
| Low Risk | 3 |
| Informational | 4 |
| **Total** | **10** |
| Audited Part | Status | Note | Mitigated |
| - | - | - | - |
| No timelock to modify contract values | Open
| Modifiable private values | Open
| Fertilizing and composting cost and reward | Open
| Required grow rate for breeding | Open
| Watering and harvesting can only be done together | Open
| Harvestable amount is calculated incorrectly | Open
| Migration from V1 can be blacklisted | Open
| Accessories are not migrated | Open
| Harvestable fertilizer amount is calculated incorrectly | Open
| Use mapping instead of looping | Open
## Introduction
## Manual Analysis
### Findings
**No timelock to modify contract values**
**Severity:** High
**Location:**
StrainzMaster.sol -> line 70-79
``` solidity
function addSeedzPool(uint256 _allocPoint, IERC20 _lpToken, bool _withUpdate) public onlyOwner {
seedzToken.add(_allocPoint, _lpToken, _withUpdate);
}
function setSeedzPool(uint256 _pid, uint256 _allocPoint, bool _withUpdate) public onlyOwner {
seedzToken.set(_pid, _allocPoint, _withUpdate);
}
function setSeedzPerBlock(uint _seedzPerBlock) public onlyOwner {
seedzToken.setSeedzPerBlock(_seedzPerBlock);
}
```
**Description:** A malicious actor in control of the MasterStrainz contract can change the contracts values at any time. Of particular note is the ability to add and change the reward rate of SEEDZ pools. The malicious actor can modify the pools in order to give themselves an unlimited quantity of SEEDZ and use that to dilute the liquidity pool for other users.
**Recommendation:** Set the contract owner to a Timelock contract to give users time to react to any changes to the contract runtime values.
-------------------------------------------------
**Modifiable private values**
**Severity:** Low
**Location:**
StrainzNFT.sol -> line 24-28
``` solidity
uint wateringPenaltyPerDay = 10; // %
uint growFactor = 255;
uint compostFactor = 100;
uint breedingCostFactor = 5;
uint breedFertilizerCost = 250;
```
**Description:** Modifiable values should be public or have custom getters for users to easily be able to check the current value of the variables.
**Recomendation:** Change values to public.
-------------------------------------------------
**Fertilizing and composting cost and reward**
**Severity:** Informational
**Location:**
StrainzNFT.sol -> lines 26 and 28
``` solidity
uint compostFactor = 100;
uint breedingCostFactor = 5;
uint breedFertilizerCost = 250;
```
StrainzNFT.sol -> line 70
``` solidity
function breed(uint _first, uint _second, bool breedFertilizer) public {
require(ownerOf(_first) == msg.sender && ownerOf(_second) == msg.sender);
StrainMetadata storage strain1 = strainData[_first];
StrainMetadata storage strain2 = strainData[_second];
uint strainzCost = (strain1.breedingCost + strain2.breedingCost) / 2;
// Burn cost
master.strainzToken().breedBurn(msg.sender, strainzCost);
uint newStrainId = mixBreedMint(strain1, strain2, breedFertilizer);
uint averageGrowRate = (strain1.growRate + strain2.growRate) / 2;
// Burn fertilizer cost
if (breedFertilizer && averageGrowRate >= 128) {
///////////////////////////////////////////////////////////////////////////////
master.seedzToken().breedBurn(msg.sender, breedFertilizerCost);
///////////////////////////////////////////////////////////////////////////////
master.strainzAccessory().breedAccessories(strain1.id, strain2.id, newStrainId);
}
emit Breed(strain1.id, strain2.id, newStrainId);
}
```
StrainzNFT.sol -> line 103
``` solidity
function compost(uint strainId) public {
require(ownerOf(strainId) == msg.sender);
StrainMetadata storage strain = strainData[strainId];
master.strainzAccessory().detachAll(strainId);
_burn(strainId);
///////////////////////////////////////////////////////////////////////////////
master.seedzToken().compostMint(msg.sender, strain.growRate * compostFactor / 100);
///////////////////////////////////////////////////////////////////////////////
emit Composted(strainId);
}
```
SeedzToken.sol -> line 24
``` solidity
uint public growFertilizerCost = 500;
```
SeedzToken.sol -> line 22
``` solidity
function buyGrowFertilizer(uint plantId) public {
uint cost = growFertilizerCost;
require(balanceOf(msg.sender) >= cost);
require(lastTimeGrowFertilizerUsedOnPlant[plantId] + 1 weeks < block.timestamp);
///////////////////////////////////////////////////////////////////////////////
_burn(msg.sender, cost);
///////////////////////////////////////////////////////////////////////////////
lastTimeGrowFertilizerUsedOnPlant[plantId] = block.timestamp;
emit FertilizerBought(msg.sender, plantId);
}
```
**Description:** 18 decimal places are used in SEEDZ. However, functions which mint and burn SEEDZ do not take this into account.
**Recommendation:** Change the noted values to reflect the correct number of decimals
-------------------------------------------------
**Required grow rate for breeding**
**Severity:** Informational
**Location:**
strainzNFT.sol -> line 69
``` solidity
function breed(uint _first, uint _second, bool breedFertilizer) public {
require(ownerOf(_first) == msg.sender && ownerOf(_second) == msg.sender);
StrainMetadata storage strain1 = strainData[_first];
StrainMetadata storage strain2 = strainData[_second];
uint strainzCost = (strain1.breedingCost + strain2.breedingCost) / 2;
// Burn cost
master.strainzToken().breedBurn(msg.sender, strainzCost);
uint newStrainId = mixBreedMint(strain1, strain2, breedFertilizer);
uint averageGrowRate = (strain1.growRate + strain2.growRate) / 2;
// Burn fertilizer cost
///////////////////////////////////////////////////////////////////////////////
if (breedFertilizer && averageGrowRate >= 128) {
///////////////////////////////////////////////////////////////////////////////
master.seedzToken().breedBurn(msg.sender, breedFertilcompostMintizerCost);
master.strainzAccessory().breedAccessories(strain1.id, strain2.id, newStrainId);
}
emit Breed(strain1.id, strain2.id, newStrainId);
}
```
**Description:** Medium suggests the average growth rate of both parents should be at least 150 but actual in the contract is 128
**Recomendation:** Update the medium or code to match.
-------------------------------------------------
**Watering and harvesting can only be done together**
**Severity:** Informational
**Location:**
StrainzNFT.sol -> line 132-142
``` solidity
function harvestAndWaterAll() public {
uint numberOfTokens = balanceOf(msg.sender);
require(numberOfTokens > 0);
uint sum = 0;
for (uint i = 0; i < numberOfTokens; i++) {
StrainMetadata storage strain = strainData[tokenOfOwnerByIndex(msg.sender, i)];
sum += harvestableAmount(strain.id) - getWateringCost(strain.id);
strain.lastHarvest = block.timestamp;
}
master.strainzToken().harvestMint(msg.sender, sum);
}
```
**Description:** Harvesting and watering are only available through the noted function. Medium post suggests that harvesting and watering individual plants is possible.
**Recommendation:** Provide additional functionality or change the medium post. Note that currently the contract assumes that watering and harvesting always happen simultaneously.
-------------------------------------------------
**Harvestable amount is calculated incorrectly**
**Severity:** Informational
**Location:**
StrainzNFT.sol -> 158-170
``` solidity
function getAccumulatedHarvestAmount(StrainMetadata storage strain) private view returns (uint) {
uint wateringRange = min(block.timestamp - strain.lastHarvest, 9 days);
uint growRate = strain.growRate * 1647058824;
uint harvestableSum = (((20 * growRate * wateringRange * 1 days) - (growRate * wateringRange * wateringRange))) / (20 * 1 days * 1 days) / 1000000000;
uint stagnationSum = 0;
if (block.timestamp - strain.lastHarvest > 9 days) {
stagnationSum = (block.timestamp - strain.lastHarvest + 9 days) * growRate * 10 / 100000000000 days;
}
return harvestableSum + stagnationSum;
}
```
**Description:** The function used to calculate the harvest amount for an NFT is incorrect. While elsewhere, the contract assumes that grow rate changes in discrete steps, here the grow rate is assumed to change linearly. Further, on line 167, `stagnationSum` has a calculation error.
**Recommendation:** Change the grow rate to be consistently discrete or linear throughout contract. Fix math errors. Provide commentary on the exact nature of the calculations.
-------------------------------------------------
**Migration from V1 can be blacklisted**
**Severity:** Medium
**Location:**
StrainzNFT.sol -> line 199-205
``` solidity
function blacklistToken(uint tokenId) public onlyMaster {
blacklist[tokenId] = true;
}
function blacklistUser(address user) public onlyMaster {
blacklistedUser[user] = true;
}
```
**Description:** A malicious actor in control of the master contract can blacklist any user or token from successfully migrating. However, the Strainz V1 tokens and NFTs will still be transferred away from the account.
**Recommendation:** Set the contract owner to a Timelock contract.
-------------------------------------------------
**Accessories are not migrated**
**Severity:** Medium
**Location:**
StrainzNFT.sol -> line 208-242
``` solidity
function migrate(address user) public onlyMaster returns (uint) {
uint numberOfStrainz = strainzV1NFT.balanceOf(user);
uint sumToHarvest = 0;
//migrate NFT
if (numberOfStrainz > 0) {
uint[] memory tokenIds = new uint[](numberOfStrainz);
for (uint i = 0; i < numberOfStrainz; i++) {
tokenIds[i] = strainzV1NFT.tokenOfOwnerByIndex(user, i);
}
for (uint i = 0; i < tokenIds.length; i++) {
StrainMetadata memory strain = getV1Strain(tokenIds[i]);
strainzV1NFT.transferFrom(user, address(this), tokenIds[i]); // burn v1
if (!blacklist[tokenIds[i]]) {
uint timeSinceLastHarvest = block.timestamp - strain.lastHarvest;
uint amountToHarvest = (strain.growRate * 255 * timeSinceLastHarvest) / 24 weeks;
// old formular
sumToHarvest += amountToHarvest;
mintTo(user, strain.prefix, strain.postfix, strain.dna, strain.generation, max(16, strain.growRate));
}
}
}
uint amountOfStrainzV1Tokens = strainzV1Token.balanceOf(user);
strainzV1Token.transferFrom(user, address(this), amountOfStrainzV1Tokens);
sumToHarvest += amountOfStrainzV1Tokens;
return blacklistedUser[user] ? 0 : sumToHarvest;
}
```
**Description:** Accessories are not migrated. Medium suggests that migration from V1 to V2 will preserve accessories.
**Recommendation:** Add migration of accessories.
-------------------------------------------------
**Harvestable fertilizer amount is calculated incorrectly**
**Severity:** Low
**Location:**
SeedzToken.sol -> line 225
``` solidity
function getHarvestableFertilizerAmount(uint strainId, uint lastHarvest) public view returns (uint) {
uint fertilizerBonus = 0;
uint fertilizerAttachTime = lastTimeGrowFertilizerUsedOnPlant[strainId];
if (fertilizerAttachTime > 0) {
uint start = max(fertilizerAttachTime, lastHarvest);
///////////////////////////////////////////////////////////////////////////////
uint end = min(start + 1 weeks, block.timestamp);
///////////////////////////////////////////////////////////////////////////////
fertilizerBonus = (end - start) * growFertilizerBoost / 1 days;
}
return fertilizerBonus;
}
```
**Description:** Fertilizers will provide a bonus for a week after last harvest once fertilized. If `start` is greater than `end` this function will revert and prevent harvesting of rewards.
**Recommendation:** Fix calculation of `end` and check for correct time range.
-------------------------------------------------
**Unbounded loop**
**Severity:** Informational
**Location:**
StrainzMultisigVault.sol -> line 51
StrainzMultisigVault.sol -> line 153
``` solidity
function signerInArray(address signer, address[] memory signerArray) private pure returns (bool) {
for (uint i = 0; i < signerArray.length; i++) {
if (signerArray[i] == signer) {
return true;
}
}
return false;
}
```
**Description:** Looping through a unbounded array could cause these functions to run out of gas if there is enough signers.
**Recommendation:** Consider using a mapping instead of looping through all the signers if possible. A mapping will always map to a value a value compared to array that could be uninitalized. A mapping lookup is executed in constant time. Another solution is to limit the signers array.
### Static
### On-chain
## Appendix A - Reviewed Documents
| Contract | Address |
| - | - |
| SeedzToken.sol | N/A |
| StrainMetadata.sol | N/A |
| StrainzAccessory.sol | N/A |
| StrainzDNA.sol | N/A |
| StrainzMarketplace.sol | N/A |
| StrainzMaster.sol | N/A |
| StrainzMultisigFactory.sol | N/A |
| StrainzMultisigVault.sol | N/A |
| StrainzNFT.sol | N/A |
| StrainzToken.sol | N/A |