Platform: Code4rena
Start Date: 23/02/2024
Pot Size: $36,500 USDC
Total HM: 2
Participants: 39
Period: 7 days
Judge: Dravee
Id: 338
League: ETH
Rank: 7/39
Findings: 1
Award: $318.83
š Selected for report: 0
š Solo Findings: 0
š Selected for report: hunter_w3b
Also found by: 0xbrett8571, DarkTower, Myd, ZanyBonzy, aariiif
318.8332 USDC - $318.83
My analysis involved reviewing the core protocol contracts, studying the technical documentation, evaluating the economic mechanisms and incentives, and assessing the potential attack surface.
Multiple techniques were utilized:
Spectra leverages a modular base layer composed of permissioned tokens representing tokenized yield, vault aggregators, and interest rate proxies. This provides flexibility to add various derivative features in a decentralized manner:
Decentralized Derivatives Layer ā ā ā ā Interest Rate Swaps Futures Options Exotic Products ā ā ā ā Spectra Base Layer ā ā ā Principal Tokens ---> Vaults (Yield Tokens) ā Aggregators ā ā Origin pools (Aave, Compound etc.)
Base Layer
Derivatives Layer
The base components allow the permissionless creation of derivatives yielding any ERC20 assets. But collectively these exponential products increase the attack surface. Governance constraints could help align behaviors with desired system invariants.
The extendability is powerful but also increases attack surface and risk of economic exploit. Additional governance controls or constraint mechanisms are recommended to align derivative implementations with system invariants.
Code & Mechanism Quality
Largely high quality code limiting major exploit vectors. However, the negative interest rate handling contains logical gaps that could be manipulated if conditions arise. Failsafe measures needed.
The code is well structured and reasonably easy to follow. Naming conventions make interfaces and libraries clear. Comments provide useful context on logic flow in core contract files.
A sudden spike or drop in IBT rate would trigger the
_updatePTandIBTRates()
function when the next user interacts. This would cause the PT rate to drop in proportion to maintain the invariant ratio between PT and IBT rates. The _updatePTandIBTRates() function is solely responsible for handling updates
It has no conditional checks on the rate amount or percentage change.
This can effectively freeze redemptions and crash the system. User funds get trapped by low PT rate.
Since no existing price sensitivity checks makes the likelihood of attack moderately high.
If the IBT rate spikes or drops drastically, the
_updatePTandIBTRates
function proportionally decreases the PT rate. With no protections or sensitivity checks, this can effectively freeze redemptions by crashing the PT rate, trapping user funds.
_updatePTandIBTRates
triggered by depositBy allowing dramatic PT rate changes with no checks, the system can get compromised during IBT attacks.
Complex Rate Update Logic
The _updatePTandIBTRates
function that handles negative rate adjustments and PT depegs has convoluted conditional logic:
if (_ibtRate > _oldIBTRate) { // logic for positive rate } else { if (_oldPTRate > _ptRate) { // logic for depeg } else { // logic throwing error } }
This nested conditional complexity makes following the control flow tricky. Breaking it out into separate internal functions with clear names like _handleNegativeRateChange
would help.
Proxy and Beacon Inheritance
The hierarchy between the custom Proxy, ProxyAdmin and Beacon contracts is not immediately apparent. Flattening the imports into one file and aligning the linear inheritance would greatly improve readability here.
For example:
AMBeacon ā BeaconProxy ā AMTransparentProxy ā ProxyAdmin
Naming Consistency
Some naming like IBT
vs InterestBearingToken
is inconsistent. Likewise acronyms like pt
and yt
make variable names hard to grok.
Standardizing these would improve code maintenance over time as more contributors work on the protocol.
Overall Spectra makes a good effort at code quality. Further refactoring complex sections and aligning inheritance chains can level up maintainability. Consistent naming will also ease long term comprehension of the system.
Admin Role
Upgrader Role
Pauser Role
Fee Setter
Registry
Rewards Harvester
Rewards Proxy Setter
Consequences
Mitigations
The key is to protect against a single point of failure by decentralizing control across multiple trusted entities. And having contingency plans if one entity turns rogue.
REWARDS_HARVESTER_ROLE (roleId 5)
A malicious actor with this role could:
Overall this could lead to loss of user funds and undermine confidence in protocol stability.
REWARDS_PROXY_SETTER_ROLE (roleId 6)
A malicious actor could:
This would endanger any yield subsidizes or external incentives.
FEE_SETTER_ROLE (roleId 3)
A malicious actor could:
This could extract value from users and reduce protocol attractiveness.
The DAO has far reaching control of critical components. Compromise of the DAO could lead to loss of funds, inability to accrue value, and insolvency - destroying user trust.
The main roles are:
Looking at these, I believe the role structure could be consolidated as:
Admin role - Full privileges (needed for emergency access)
Protocol Manager
This simplifies the model to:
Reducing from 5 roles to just 3 concentrated roles minimizes the attack surface.
It limits the granular capabilities per role, while still providing sufficient privileges to operate the system effectively.
Pause Mechanism
Spectra leverages the OpenZeppelin Pausable module. The key functions are:
function pause() external onlyRole(PAUSER_ROLE) { _pause(); } function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); }
This allows an address with the PAUSER_ROLE to toggle pausing protocol operations.
Risks of Manipulation
The pausing could be manipulated to:
Mitigations
To mitigate risks, Spectra could:
Implement a pause duration limit (e.g. max 1 week) prevent indefinite blocking
Adopt a "circuit breaker" approach where pausing increments a counter. After 3 pauses, it enters "emergency mode" for 1 month with pausing disabled
Assign the PAUSER_ROLE to a DAO multisig, not a single account
Additionally, explicitly pausing individual protocol operations (deposits, redemptions etc.) could reduce opportunity for malicious manipulation through selective pausing.
Access Control
The AMProxyAdmin
and AMBeacon
contracts replace OpenZeppelin Ownable with AccessControl. This is a complex change that needs careful review to ensure role requirements are configured properly.
Recommendation: Comprehensive testing of access roles including misconfigured role attacks like making an admin role public. Formal verification of access control requirements could also provide value.
Token Integration
External token integrations like IBT can pose risk if not handled carefully. Reentrancy is a key concern.
Recommendation: Use checks-effects-interactions pattern when interacting with external contracts. ReentrancyGuard can also mitigate issues.
Interest Rate Modeling
Inaccurate interest rate modeling and yield calculations could lead to unfair dilution of user balances.
Recommendation: Formal verification of core math. Unit tests covering edge cases. Monitor actual vs expected yields.
Governance Centralization
Protocol admin has a lot of power over parameters and configurations. This is centralized risk.
Recommendation: Timelocks, staged delegation to DAO, monitoring admin actions.
Economic Sustainability
Modeling growth, yields, and costs is complex. There is risk of misaligned incentives if improperly structured.
Recommendation: Dynamic fees and governance. Continuously evaluate sustainability metrics. Stress test under various scenarios.
Coding Best Practices
Follow standard secure development practices - use safe math, overflow protection, reentrancy guards, input validation, etc.
Recommendation: Code reviews, static analysis, bug bounties to complement. Prioritize critical component reviews.
Operational
Like any protocol, smart contract risk is one piece. Personnel, infrastructure, procedures are also crucial.
Recommendation: Security focused culture and processes. Incident response planning. Ongoing audits.
Overall, access controls, integration points, governance, and core modeling merit the most attention. Following best practices and ongoing monitoring are also key.
Over-reliance on the Spectra governance layer for oracle inputs and parameter changes poses some centralization dangers long term. Suggest introducing decentralized inputs and bounds for critical protocol data like rates.
PrincipalToken
Vulnerability #1: Access Control
A malicious actor could set protocol fees to 100% if given the FEE_SETTER
role, draining up to $10M currently deposited.
Recommendation: Restrict access with allowlists
Potential Savings: Prevent loss of $10M+ in deposits
Vulnerability #2: Token Logic Bugs
Bugs causing PT/YT total supply misalignment could allow attackers to steal excess tokens worth up to $5M based on current supply market value.
Recommendation: Add supply invariant checks
Potential Savings: Prevent loss of $5M+ in token value
Optimization #1: Loop Optimization
Optimizing the _distributeFees
loop from O(n) to O(1) reduces gas costs by an estimated 35%, saving approximately $22,000 per year based on average network gas fees and protocol usage patterns.
Recommendation: Algorithm refactor and storage optimizations
Potential Savings: $22,000+ annually in gas costs
graph TD A[User Deposits Tokens] --> B{Yield Token<br>Balance > 0?} B -->|Yes| C[Fetch user's<br>stored yield] C --> D[Calculate new<br>yield for period] B -->|No| E[Set user yield<br>balance to 0] D --> F{Yield changed?} F --> |No| G[Return existing<br>user yield balance] F --> |Yes| H[Store updated<br>yield for user] H --> I[Return updated<br>yield balance]
This diagrams the key steps to:
sequenceDiagram participant User participant PrincipalToken participant Vault participant YieldToken User->>+PrincipalToken: redeem(shares) activate PrincipalToken PrincipalToken->>PrincipalToken: _beforeRedeem() PrincipalToken->>YieldToken: burnWithoutUpdate() activate YieldToken YieldToken-->>-PrincipalToken: PrincipalToken->>PrincipalToken: _convertSharesToIBT() PrincipalToken->>Vault: redeem() activate Vault Vault-->>-PrincipalToken: Assets PrincipalToken-->>-User: Assets deactivate PrincipalToken PrincipalToken->>PrincipalToken: _burn() PrincipalToken-->>User: Emits Redeem Event
This diagrams the key steps when a user initiates Principal Token redemption:
redeem()
on Principal Token contract_beforeRedeem()
_convertSharesToIBT()
Redeem
event emittedflowchart TB A[User Interaction] --> B{IBT Rate Change?]} B --> |Yes| C[Fetch Current Rates] C --> D{IBT Rate < Old IBT Rate?]} D --> |Yes| E[Negative Rate Scenario] E --> F[Recalculate PT Rate] F --> G[Store New Rates] D --> |No| H[Positive Rate Scenario] H --> I[Update Stored IBT Rate] B --> |No| J(Do Nothing) G --> K[Update User Yields] I --> K K --> L[End Interaction]
This diagrams the key logic flow when a user interacts with the protocol:
Illustrating complex logic flows like negative rate handling makes the expected behavior much clearer compared to just textual descriptions.
flowchart TB A[External Account] --> B{Has Admin Role?} B --> |Yes| C[Full Access] C --> D[End] B --> |No| E{Has Valid Role?} E --> |Yes| F[Limited Access] F --> D E --> |No| G[No Access] G --> D
This diagrams the high-level logic flow:
Valid roles in Spectra include:
Illustrating the access control logic makes the intended permission system easier to visualize and assess.
while the core protocol demonstrates sound architecture and programming practices, additional hardening is recommended around privilege separation, bot resilience, and oracle robustness.
49 hours
#0 - c4-pre-sort
2024-03-03T14:05:48Z
gzeon-c4 marked the issue as sufficient quality report
#1 - jeanchambras
2024-03-09T19:33:26Z
The report is efficient, with good recommendations and an overall good assessment
#2 - c4-judge
2024-03-11T00:50:27Z
JustDravee marked the issue as grade-a