Spheron Network security record

As of 30 Oct 2026, Spheron Network has 3 audit reports by 1 security firm on Trustblock; the latest is by QuillAudits, dated 27 Mar 2025. Those reports list no open critical findings.

Critical findings
No open critical findings
Across 3 reports.
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.

Audits3

Newest first

AuditSecurity firmDateScopeCritical findingsReport
Spheron change review

Audit · firm’s term: review

Security firm: QuillAuditsDate: Scope: Files named in the reportNo open critical findingsOpen · quillaudits.com
Spheron V2Security firm: QuillAuditsDate: Scope: Files named in the reportNo open critical findingsOpen · quillaudits.com
Spheron V1Security firm: QuillAuditsDate: Scope: Files named in the reportNo open critical findingsOpen · quillaudits.com

Findings28

Statuses are the security firm’s own, from each report.

SeverityOpenAcknowledgedFixed
Critical000
High003
Medium605
Low1103
HighLiquidation doesn’t accrue interestSpheron V2 · QuillAudits · 5 Mar 2025Fixed

accureInterest() should be called whenever totalDebt is being used because it updates the totalDebt. Now debtOf(agent) uses totalDebt to calculate the debtOf(agent), which means totaltotalDebt should be updated before calling debtOf(agent) in startLiquidation(). ie missing accureIntest()<br><code>solidity function startLiquidation(address agent) external nonReentrant whenNotPaused { //@audit this should also call accureIntest() if (msg.sender != core) revert CallerNotCore(); @> uint256 liquidatingAmount = debtOf(agent); removeDebt(agent, liquidatingAmount); totalLiquidatingAmount += liquidatingAmount; liquidatingAmounts[agent] += liquidatingAmount; invariantCheck(); emit LiquidationStarted(agent, liquidatingAmount); } </code>

Open the report
HighSignature replay protection bypassed also due to wrong hashing implementationSpheron V2 · QuillAudits · 5 Mar 2025Fixed

In verifySignature, the check if (nonce != core.signatureNonces(agent)) revert InvalidNonce(); was to help prevent replays as the purpose of nonce logic is to prevent signature replay however this nonce is also a user argument, what that means is user specifies this value and not the protocol. The issue is the nonce specified by the user is not included when generating the hash to be verified allowing user to specify a different nonces but on the same signature.

Open the report
HighWrong hashing implementation allows user to execute EXPIRED signatureSpheron V2 · QuillAudits · 5 Mar 2025Fixed

In the bootstrapOffchainCollateral(), you'll notice it checks if (block.timestamp > deadline) revert ExpiredSignature(deadline); for expired signature however also note this deadline is a user argument, the issue here is that the deadline was not included when generating the hash to verify, so even if the signature signed by the core signers is expired, user can just pass in a future time which will always pass since the deadline won't get validated

Open the report
MediumwithdrawEarnings can be initiated by anyoneSpheron change review · QuillAudits · 27 Mar 2025Fixed

The below function can be initiated by anyone by passing isFizz = true and _fizzId of anyone to initiate withdrawal without consent.The first if block will just transferEarnedTokens to the rewardWallet. No loss of tokens since the transfer is done to the rewardWallet but withdrawals can be initiated without consent.

Open the report
MediumDOS due to check implemented in wrong function causes changeAgentOwner to always revertSpheron V2 · QuillAudits · 5 Mar 2025Fixed

The changeAgentOwner() function allows owner to change it's agent owner, it has the isAgentCall modifier what this means is that only the Agent can call this function and not the owner, also meaning the owner would have to call this through the agent contract which is the intended design. However, an issue arises in the corelogic.changeAgentOwner, notice the check if (agentDetail.owner != msg.sender) revert CallerNotAgentOwner(); this means caller must be the owner itself and not the agent, this is a conflict check to the isAgentCall modifier causes the entire function to always revert

Open the report
MediumHonest users can be griefed in withdrawRequest.Spheron V2 · QuillAudits · 5 Mar 2025Fixed

User can initiate withdrawRequest on behalf of other user by passing _to, and this will increase the<br>unlockTime of withdrawRequest unlockTime is checked in withdraw().<br><code>solidity function withdrawRequest(uint256 _amount, address _to) external whenNotPaused { require(_to != address(0), "Invalid recipient"); require(_amount > 0, "Invalid amount"); WithdrawRequest storage request = withdrawRequests[_to]; pToken.transferFrom(msg.sender, address(this), _amount); uint256 shares = pToken.convertToShares(_amount); request.shares += shares; //@audit this can be used to grief other users by initiating a withdrawRequest and below line will increase the unlockTime by vestingTime @> request.unlockTime = block.timestamp + vestingTime; emit WithdrawRequested(_to, shares); } function withdraw() external whenNotPaused { address _to = msg.sender; WithdrawRequest storage request = withdrawRequests[_to]; require(request.shares > 0, "No withdraw request found"); @> require(block.timestamp >= request.unlockTime, "Vesting time not reached"); uint256 amount = pToken.convertToAssets(request.shares); delete withdrawRequests[_to]; pToken.approve(address(delegationPool), amount); delegationPool.withdraw(amount, _to); emit Withdraw(_to, amount); } </code>

Open the report
MediumUpdateFeeRate doesn’t accrue old fee before updateSpheron V2 · QuillAudits · 5 Mar 2025Fixed

DelegationPool.sol.updateFee() doesn't accrue old fee before updating it to the new one<br><code>solidity function updateFeeRate(uint256 _newFeeRate) external onlyOwner { feeRate = _newFeeRate; emit FeeRateUpdated(_newFeeRate); } </code><br>as the updateFeeRate function doesn't accrue fees before the update, there will be problem in distributeLiquidationSurplus and accrueInterest functions as both distribute pTokens w.r.t fee. Although<br>distributeLiquidationSurplus won't be affected much since it is only called when liquidation is called but accrued interest will be affected since it is dependent on the fee.<br><code>solidity function distributeLiquidationSurplus(uint256 amount) internal { uint256 fee = (amount * feeRate) / 10_000; pToken.mint(feeReceiver, fee); pToken.distribute(amount - fee); } function accrueInterest() public { uint256 timeElapsed = block.timestamp - lastAccrueTimeStamp; if (timeElapsed == 0) return; uint256 totalInterestIncrement = ( interestRateModel.getBorrowRate(pToken.totalSupply(), totalDebt + totalLiquidatingAmount) * totalDebt * timeElapsed ) / SECONDS_PER_YEAR / 1e18; totalDebt += totalInterestIncrement; uint256 fee = (totalInterestIncrement * feeRate) / 10_000; lastAccrueTimeStamp = block.timestamp; pToken.mint(feeReceiver, fee); pToken.distribute(totalInterestIncrement - fee);//@audit if fee is increased then less pTokens will be distributed here invariantCheck(); } </code><br>The correct way would be to collect the fee first and then update the fee, or call the accrueInterest()<br>before updating the fee.<br>

Open the report
MediumWithdraw doesn't enforce slippage protectionSpheron V2 · QuillAudits · 5 Mar 2025Fixed

There should be slippage protection for users otherwise while withdrawing they could lose significant amount if exchange rate is high/low<br><br><code>solidity function withdrawRequest(uint256 _amount, address _to) external whenNotPaused { require(_to != address(0), "Invalid recipient"); require(_amount > 0, "Invalid amount"); WithdrawRequest storage request = withdrawRequests[_to]; pToken.transferFrom(msg.sender, address(this), _amount); @> uint256 shares = pToken.convertToShares(_amount); request.shares += shares; request.unlockTime = block.timestamp + vestingTime; emit WithdrawRequested(_to, shares); } function withdraw() external whenNotPaused { address _to = msg.sender; WithdrawRequest storage request = withdrawRequests[_to]; require(request.shares > 0, "No withdraw request found"); require(block.timestamp >= request.unlockTime, "Vesting time not reached"); uint256 amount = pToken.convertToAssets(request.shares); delete withdrawRequests[_to]; pToken.approve(address(delegationPool), amount); delegationPool.withdraw(amount, _to); emit Withdraw(_to, amount); } </code><br>

Open the report
LowHardcoded claimDeadLine will lead to loss of vesting assets for usersSpheron V2 · QuillAudits · 5 Mar 2025Fixed

In AirdropPool.sol the constructor sets the claimDeadline as claimDeadline = _launchTime + (8 * 30 days); which is hardcoded as 240 days from the launchTime, However the function claimVestingAssets has a check : require(block.timestamp <= claimDeadline, "Claim period has ended"); Which means the claim can only be made when the claimDeadline is not passed but the claim deadline is hardcoded to 240 days in the constructor, and for the periods of more than 240 days vesting schedule users won't be able to claim vesting assets via claimVestingAssets function.<br><code>solidity function claimVestingAssets() external whenNotPaused { UserInfo storage user = claimInfo[msg.sender]; require(user.claimed, "Not claimed"); require(block.timestamp <= claimDeadline, "Claim period has ended"); require(user.totalShares > user.claimedShares, "No shares to claim"); uint256 claimableShares = _claimAfterLockTime(); user.lastClaimTime = block.timestamp; pToken.transfer(msg.sender, pToken.convertToAssets(claimableShares)); emit AirdropClaimed(msg.sender, pToken.convertToAssets() </code>

Open the report
LowWrong event emitted in the repay functionSpheron V2 · QuillAudits · 5 Mar 2025Fixed

The repay() function in delegation pool emits wrong event<br><code>diff- emit Repaid(agent, payingAmount, payingAmount); + emit Repaid(agent, tokenAmount, payingAmount); </code>

Open the report
Show 1 more
LowWrong getAgentAddress() check in registerMiner()Spheron V2 · QuillAudits · 5 Mar 2025Fixed

The check if (getAgentAddress(minerId, _msgSender()) != address(0)) revert AgentAlreadyCreated(); in registerMiner() is implemented wrongly, notice getAgentAddress(minerId, _msgSender()), it passes minerId first then _msgSender() , However this is the getAgentAddress() function: function getAgentAddress(address _owner, address _minerId) public view returns (address) {<@ note owner first then minerId<br>} As we can see the parameters where passed in wrongly leading to wrong value returned<br>

Open the report

Contracts

No deployed address is recorded for these audits.

The reports name a repository or files. Open a report to see its scope.

The repositories and files the reports name

Show this on your site

Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.

Content

Facts

Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-tb-project-slug="argoapp"
></div>
</body>

Paste the code where the label should appear. It uses no API credit and updates by itself.

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us