Turtle Club Protocol
Cantina Security Report
Organization
- @turtle-club
Engagement Type
Cantina Reviews
Period
-
Repositories
Researchers
Findings
Low Risk
2 findings
2 fixed
0 acknowledged
Informational
5 findings
5 fixed
0 acknowledged
Gas Optimizations
1 findings
1 fixed
0 acknowledged
Low Risk2 findings
TokensRescuedFromStream event could be emitted with incorrect value
State
- Fixed
PR #38
Severity
- Severity: Low
Submitted by
Chinmay Farkya
Description
In
rescueTokensFromStream()function inStreamFactory.sol, the following event is emitted :function rescueTokensFromStream( IStream stream, IERC20 token, address payable to, uint amount ) external restricted validStream(stream) { stream.rescueTokens(token, to, amount); emit TokensRescuedFromStream(stream, token, to, amount);Here, the
amountis passed on as function input and emitted as it is. But we can see in Stream.rescueTokens() logic that actual amount that is rescued can be different from the input amount.if (balance < amount) amount = balance;In this edge case, the
amountemitted inTokensRescuedFromStream()is incorrect.Recommendation
Consider modifying
rescueTokensFromStream()function such that it emits the actual amount of the token rescued.Missing zero address check in withdrawFunds() function
State
- Fixed
PR #38
Severity
- Severity: Low
Submitted by
Chinmay Farkya
Description
In
Stream.sol, the stream owner is allowed to withdraw therewardTokenbalance, but it is not checked anywhere if thetoaddress provided is a valid address.If the owner uses address(0) as the
toaddress by mistake, the full rewardToken balance will be lost.The withdrawal flow from
StreamFactory => Stream.withdrawFunds()also lacks this check.Note : Not all tokens revert on transferring to address(0), some tokens allow this.
Recommendation
Consider adding this check to
Stream.sol::withdrawFunds(), just like how its done inStream.sol::rescueTokens()if (to == address(0)) revert InvalidToAddress();
Informational5 findings
Incorrect comments
State
- Fixed
PR #38
Severity
- Severity: Informational
Submitted by
Chinmay Farkya
Description
There are several instances of misleading/ incorrect comments across the codebase :
- StreamFactory.sol #L66 :
Mapping to record stream info by stream IDshould be changed to =>Mapping to record stream info by stream address - StreamFactory.sol #L68 :
Array of all created stream IDs=>Array of all created stream addresses - StreamFactory.sol #L70 :
Mapping to map stream contract address to stream ID=>Mapping to map stream ID to stream contract address - StreamFactory.sol #L98 :
only the admin or authorized roles can unpause=>only the admin or authorized roles can pause, unpause and changeStreamAdmin
Recommendation
Consider modifying the comments as noted.
renounceOwnership() should be blocked to prevent accidentally losing contract ownership
State
- Fixed
PR #38
Severity
- Severity: Informational
Submitted by
Chinmay Farkya
Description
The
Stream.solcontract inherits from OpenZeppelin's Ownable2StepUpgradeable library, and has an owner address responsible for important admin operations.For standalone stream instances deployed independently of the factory, the Stream owner will be controlled by the stream admin. If
renounceOwnership()is called by current owner by mistake, this can lead to permanent loss of ownership, which can create issues with the stream's operations.Recommendation
Consider overriding
renounceOwnership()inStream.solto always revert.Stream implementation upgrade might not work properly
Severity
- Severity: Informational
Submitted by
Chinmay Farkya
Description
In
StreamFactory.sol, the protocol admin can set a new stream implementation address, which will be used in future deployments of new stream clones. This can be done usingsetStreamImplementation()function.But the new implementation might have different initialization logic, preventing compatibility with the current
StreamFactory. This is because when initializing the Stream clone increateStream(), the initData passed to the new stream clone is hardcoded in current factory.function _createStream( CreateStreamParams calldata params, uint40 deadline, bytes calldata signature ) private returns (IStream stream) { StreamFactoryStorage storage $ = _getStreamFactoryStorage(); /*** Deploy stream ***/ // Encode initialization data bytes memory initData = abi.encodeWithSelector( IStream.initialize.selector, params.streamId, params.rewardToken ); // Deploy proxy stream = IStream(address(new ERC1967Proxy(address($.streamImplementation), initData)));If protocol were to create a new stream implementation which has a different selector for its
initialize()function, all new stream clone deployments would fail due to selector mismatch.Recommendation
Consider documenting the limitations of the approach used for upgrading stream implementation in the current factory, as some types of modifications in the stream logic will not work with the current factory version.
Interface documentation for batchClaim and batchClaimFor is inconsistent with the implementation
Severity
- Severity: Informational
Submitted by
slowfi
Description
The interface documentation for
batchClaimandbatchClaimForstates@dev Modifiers: nonReentrant. However, the corresponding implementations inStreamFactorydo not apply anynonReentrantguard (nor an equivalent custom reentrancy protection).This creates an inconsistency between the interface’s NatSpec and the actual runtime behavior.
Recommendation
Consider to either:
- remove/update the outdated NatSpec comment in
IStreamFactoryto match the implementation, or - add an explicit reentrancy guard in
StreamFactoryif the original design intent was to enforcenonReentrantsemantics for these entrypoints.
Unused ReentrancyGuardUpgradeable import in Stream
Severity
- Severity: Informational
Submitted by
slowfi
Description
The contract
StreamimportsReentrancyGuardUpgradeable(src/Stream.sol:7), but the contract does not inherit from it nor use any of its modifiers.Based on the client’s clarification, this was an intentional design decision after determining that no reentrancy risks are present, and the guard is therefore unnecessary.
Recommendation
Consider to remove the unused
ReentrancyGuardUpgradeableimport to keep the codebase minimal and aligned with the intended design.
Gas Optimizations1 finding
Input validation in createStream() can be re-arranged
State
- Fixed
PR #38
Severity
- Severity: Gas optimization
Submitted by
Chinmay Farkya
Description
In
createStream()function, the signature is verified first and input validation is done later for theCreateStreamParams.function createStream( CreateStreamParams calldata params, uint40 deadline, bytes calldata signature ) external returns (IStream stream) { _validateCreateStreamSignature(params, deadline, signature); _validateCreateStreamParams(params);Checking malformed input params first will potentially save gas, so we can re-arrange the logic to first check
_validateCreateStreamParams(), followed by_validateCreateStreamSignature().Recommendation
Consider checking
CreateStreamParamsfirst, before trying to verify the signature.