Prepared by:
HALBORN
Last Updated 04/26/2024
Date of Engagement by: April 19th, 2022 - April 29th, 2022
88% of all REPORTED Findings have been addressed
All findings
8
Critical
2
High
0
Medium
1
Low
1
Informational
4
\client engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-04-19 and ending on April 29th, 2022. The security assessment was scoped to the smart contracts provided to the Halborn team.
The smart contracts allowed for staking and unstaking NFTs on the Lighthouse platform, as well as claiming the rewards.
The team at Halborn was provided one week for the engagement and assigned a full-time security engineer to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.
The purpose of this audit is to:
Ensure that smart contract functions operate as intended
Identify potential security issues with the smart contracts
In summary, Halborn identified some security risks that should the contracts be deployed as they are, users would risk losing their staked assets. These issues stemmed from simple code mistakes, which prevent users who staked an asset from unstaking it and claiming their rewards. Additionally, due to an inverted if statement on the imported code that the smart contracts rely on, when unstaking an asset, they will always be burned.
Halborn recommends that the code is reviewed by the Seascape team and that the required corrections are implemented. Furthermore, it is recommended that thorough test cases are implemented to identify all possible errors with the code and extreme cases that users might encounter.
In summary, Seascape fixed the majority of the issues described in the report.
Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy regarding the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation, automated testing techniques help enhance coverage of the bridge code and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the audit:
Research into architecture and purpose
Smart contract manual code review and walkthrough
Graphing out functionality and contract logic/connectivity/functions (solgraph
)
Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes
Manual testing by custom scripts
Static Analysis of security for scoped contract, and imported functions (Slither
)
Testnet deployment (Brownie
, Remix IDE
)
The assessment was scoped to the LightHouseStake.sol
and StakeNftInterface.sol
contracts available at \href{https://github.com/Seastarinteractive/lighthouse-smartcontracts/tree/nft-staking/contracts}{GitHub}.
The commit ID at the time of the assessment was the following: 54fdb88df32b974370f10d2e50b8f9a80a6bdb47
The commit ID of the fixes were the following:
d4c66554ddedffd06e10927f4aab05a35e08368f - \href{https://github.com/Seastarinteractive/lighthouse-smartcontracts}{LighthouseStake}
99c803ffff80aae53405f8460f3373ee8a2386d2 - \href{https://github.com/Seastarinteractive/lighthouse-smartcontracts}{LighthouseStake}
faa1bd6609fd6443e4e4d7d082166012176b9ad1 - \href{https://github.com/blocklords/seascape-smartcontracts/tree/main/contracts/defi}{Seascape Smart Contracts}
Critical
2
High
0
Medium
1
Low
1
Informational
4
Impact x Likelihood
HAL-01
HAL-02
HAL-04
HAL-03
HAL-05
HAL-06
HAL-07
HAL-08
Security analysis | Risk level | Remediation Date |
---|---|---|
NFT OWNERS CANNOT UNSTAKE OR CLAIM REWARDS | Critical | Solved - 05/09/2022 |
NFT WILL ALWAYS BE BURNED WHEN UNSTAKING | Critical | Solved - 05/19/2022 |
NEW STAKING SESSIONS CANNOT BE CREATED | Medium | Solved - 05/09/2022 |
MISSING REENTRANCY GUARD | Low | Solved - 05/09/2022 |
UNCLEAR ERROR HANDLING | Informational | Solved - 05/09/2022 |
INSUFFICIENT TESTS | Informational | Acknowledged |
MISLEADING COMMENTS | Informational | Solved - 05/09/2022 |
UNUSED VARIABLE | Informational | - |
// Critical
After staking, users are neither able to withdraw their NFT nor claim the rewards due to a mistake in the code. This will lead to the asset being locked forever.
LightHouseStaking.sol
Lines# 157 and 177.
function unstake(uint256 sessionId, uint256 nftId) external {
address staker = msg.sender;
Session storage session = sessions[sessionId];
require(session.rewardPool > 0, "session does not exist");
Player storage playerChallenge = playerParams[sessionId][nftId];
require(playerChallenge.player != msg.sender, "forbidden");
StakeNft handler = StakeNft(stakeHandler);
handler.claim(sessionId, staker);
handler.unstake(sessionId, staker, nftId, false);
delete playerParams[sessionId][nftId];
emit Unstake(staker, sessionId, nftId);
}
/// @notice CLAIMING:
/// you can't call this function is time is completed.
/// you can't call this function if nft is burning.
function claim(uint256 sessionId, uint256 nftId) external {
Session storage session = sessions[sessionId];
address staker = msg.sender;
Player storage playerChallenge = playerParams[sessionId][nftId];
require(session.rewardPool > 0, "session does not exist");
require(playerChallenge.player != msg.sender, "forbidden");
StakeNft handler = StakeNft(stakeHandler);
uint256 claimed = handler.claim(sessionId, staker);
emit Claim(staker, sessionId, nftId, claimed);
}
SOLVED: The Seascape team
amended the smart contract as suggested.
\newpage
// Critical
Testing revealed that NFTs will always be burned when unstaked due to an error in the StakeNft.sol
contract, which burns the token when the variable burn
is set to false. Users might not be aware of this when staking their NFT, thus resulting in the loss of their asset.
LightHouseStake.sol
Lines #151-166
function unstake(uint256 sessionId, uint256 nftId) external {
address staker = msg.sender;
Session storage session = sessions[sessionId];
require(session.rewardPool > 0, "session does not exist");
Player storage playerChallenge = playerParams[sessionId][nftId];
require(playerChallenge.player != msg.sender, "forbidden");
StakeNft handler = StakeNft(stakeHandler);
handler.claim(sessionId, staker);
handler.unstake(sessionId, staker, nftId, false);
delete playerParams[sessionId][nftId];
emit Unstake(staker, sessionId, nftId);
}
StakeNft.sol
Lines #79-99
function unstake(uint key, address stakerAddr, uint id, bool burn)
external
nonReentrant
{
address stakeToken = periods[msg.sender][key].stakeToken;
IERC721 nft = IERC721(stakeToken);
require(nft.ownerOf(id) == address(this), "not owned");
require(owners[msg.sender][key][id] == stakerAddr);
delete owners[msg.sender][key][id];
delete weights[msg.sender][key][id];
if (burn) {
nft.safeTransferFrom(address(this), stakerAddr, id);
} else {
nft.safeTransferFrom(address(this), 0x000000000000000000000000000000000000dEaD, id);
}
withdraw(key, stakerAddr, weights[msg.sender][key][id]);
}
SOLVED: The Seascape team
amended the contracts to remove the burning functionality. Every time a user unstakes their ERC721, it will not be burned, but transferred to them.
\newpage
// Medium
Within the addSession
function of LighthouseStake.sol
, the startTime
of the session needs to be after the current timestamp, thus in the future. However, this function calls the newPriod
function of StakeNft.sol
which subsequently calls the newStakePeriod
of Stake.sol
. Within this last smart contract's function, a check is enforced that the startTime
is before or equal to the current timestamp, thus in the past. This is contradictory as the functions are called one after the other, and this will result in the session not being created.
LighthouseStake.sol
Lines #67-104
[snip]
require(
rewardPool > 0 && period > 0 && startTime > block.timestamp,
"zero_value"
);
[snip]
stakeNft.sol
Lines #33-56
[snip]
newStakePeriod(key, startTime, endTime, rewardPool);
[snip]
\newpage
Stake.sol
Lines #45-51 and #72-91
modifier validStakePeriodParams(uint key, uint startTime, uint endTime, uint rewardPool) {
require(startTime <= block.timestamp, "STAKE_TOKEN: invalid_start");
require(startTime < endTime, "STAKE_TOKEN: invalid_time");
require(rewardPool > 0, "STAKE_TOKEN: zero_value");
require(stakePeriods[msg.sender][key].startTime == 0, "STAKE_TOKEN: period_exists");
_;
}
function newStakePeriod(
uint key, // a unique identifier. could be a session id.
uint startTime,
uint endTime,
uint rewardPool // if usdc, then decimals is 9 for reward pool.
// if cws, then decimals is 18 for reward pool.
)
internal
validStakePeriodParams(key, startTime, endTime, rewardPool)
{
// Challenge.stake is not null, means that earn is not null too.
StakePeriod storage period = stakePeriods[msg.sender][key];
period.rewardPool = rewardPool;
period.startTime = startTime;
period.endTime = endTime;
period.unit = rewardPool * SCALER / (endTime - startTime);
period.rewardClaimableTime = startTime;
emit NewStakePeriod(msg.sender, key, startTime, endTime);
}
SOLVED: The imported contract was amended to ensure that the startTime
is in the future.
\newpage
// Low
During the audit, it was discovered that all the public functions within the smart contract LighthouseStake.sol
such as stake
, unstake
and claim
did not implement protections against reentrancy attacks. To protect against reentrancy attacks, it may be necessary to use a mutex. By using this lock, an attacker can no longer exploit the withdrawal function with a recursive call. OpenZeppelin has its own mutex implementation called ReentrancyGuard
which provides a modifier to any function called nonReentrant
that protects the function against re-entrancy attacks.
The affected functions were in the file LighthouseStake.sol
stake
unstake
claim
SOLVED: The nonReentrant
modifier was added to highlighted functions.
\newpage
// Informational
Within the addSession
function of the LighthouseStake
contract, one require
statement does not produce the appropriate error message. Three different conditions are checked in this statement, and the error message only effectively describes two of them.
LighthouseStake.sol
- Lines #80-83
require(
rewardPool > 0 && period > 0 && startTime > block.timestamp,
"zero_value"
);
SOLVED: The Seascape team
amended the code as suggested.
\newpage
// Informational
It was identified that the tests included within the repository were not sufficient. Should Seascape have implemented more thorough unit tests, certain issues highlighted within this report would have been identified.
ACKNOWLEDGED: The Seascape team
did not implement more extensive test cases for NFT staking contracts.
\newpage
// Informational
Within the LigthouseStake
contract, the stake
function is described as taking a signature and the amount of assets staked. However, the function definition does only accept the sessionId
and nftId
parameters. Furthermore, the comment description of the unstake
and claim
functions do not reflect the parameters passed to the function and their logic.
Should the code called by these functions require these parameters, they would not be provided, and the code might not work as expected. Furthermore, this reduces readability of the code and could introduce errors in code that depend on this smart contract.
LighthouseStake.sol
Lines #106-146
/// @notice Stake an and some token.
/// For the first time whe user deposits his :
/// It receives id, signature and amount of staking.
function stake(uint256 sessionId, uint256 nftId) external
[snip]
LighthouseStake.sol
Lines #148-166
/// @notice Unstake nft. If the challenge is burning in this sesion
/// then burn nft.
/// @dev data variable is not used, but it is here for following the ZombieFarm architecture.
function unstake(uint256 sessionId, uint256 nftId) external
[snip]
SOLVED: The Seascape team
amended the comment to reflect the functionality of highlighted functions.
\newpage
// Informational
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed