Platform: Code4rena
Start Date: 15/07/2021
Pot Size: $80,000 USDC
Total HM: 28
Participants: 18
Period: 7 days
Judge: ghoulsol
Total Solo HM: 18
Id: 20
League: ETH
Rank: 4/18
Findings: 7
Award: $6,414.99
🌟 Selected for report: 3
🚀 Solo Findings: 1
shw
The _approve
functions of the pool LP tokens and synths do nothing if the _allowances
is already the maximum number, i.e., type(uint256).max
. Therefore, Alice cannot change her allowance to Bob once she approved him with the maximum approval.
Referenced code: Pool.sol#L99 Synth.sol#L93
Consider removing the _allowances[owner][spender] < type(uint256).max
condition of _approve
to allow users to reset their allowance to others even if it is the maximum.
#0 - SamusElderg
2021-07-25T11:49:50Z
Duplicate of #29
shw
The return values of BEP20.transfer
and BEP20.transferFrom
are not checked to be true
in multiple contracts. The return value could be false
if the transferred token is not BEP20-compliant, indicating that the transfer fails, while the calling contract will not notice the failure. As written in the BEP20 specification:
Callers MUST handle false from returns (bool success). Callers MUST NOT assume that false is never returned!
The return value check should be applied, especially when the transferred token is not created by the Spartan protocol (and thus could be non-compliant to BEP20):
Unchecked transfer
:
Pool.sol#L199
Pool.sol#L224
Router.sol#L67
Router.sol#L126
Router.sol#L221
Unchecked transferFrom
:
poolFactory.sol#L112
Router.sol#L207
Synth.sol#L204
Dao.sol#L266
Use the SafeBEP20
implementation from PancakeSwap (or SafeERC20
from OpenZeppelin) and call safeTransfer
or safeTransferFrom
when transferring BEP20 tokens.
#0 - SamusElderg
2021-07-26T01:46:23Z
Duplicate of #8
🌟 Selected for report: shw
3942.0057 USDC - $3,942.01
shw
The getPoolShareWeight
function returns a user's pool share weight by calculating how many SPARTAN the user's LP tokens account for. However, this approach is vulnerable to flash loan manipulation since an attacker can swap a large number of TOKEN to SPARTAN to increase the number of SPARTAN in the pool, thus effectively increasing his pool share weight.
According to the implementation of getPoolShareWeight,
a user's pool share weight is calculated by uints * baseAmount / totalSupply
, where uints
is the number of user's LP tokens, totalSupply
is the total supply of LP tokens, and baseAmount
is the number of SPARTAN in the pool. Thus, a user's pool share weight is proportional to the number of SPARTAN in the pool. Consider the following attack scenario:
baseAmount
. He could split his trade into small amounts to reduce slip-based fees.DaoVault
. He adds his LP tokens to the pool by calling the deposit
function of Dao.
Dao
then calls depositLP
of DaoVault
, causing the attacker's weight to be recalculated. Due to the large proportion of SPARTAN in the pool, the attacker's weight is artificially increased.harvest
of the Dao
contract.Referenced code: Utils.sol#L46-L50 Utils.sol#L70-L77 DaoVault.sol#L44-L56 Dao.sol#L201 Dao.sol#L570
A possible mitigation is to record the current timestamp when a user's weight in the DaoVault
or BondVault
is recalculated and force the new weight to take effect only after a certain period, e.g., a block time. This would prevent the attacker from launching the attack since there is typically no guarantee that he could arbitrage the WBNB back in the next block.
#0 - SamusElderg
2021-07-26T03:53:56Z
Recommended mitigation has been included in contributors ongoing discussions to make this more resistant to manipulation
#1 - ghoul-sol
2021-08-08T22:41:05Z
Keeping high risk because of impact
shw
The functions of creating new DAO proposals (e.g., newActionProposal
) are permissionless. Anyone can create a new proposal by paying some fees in SPARTA, as long as the previous proposal is closed. Thus, an attacker could then front-run proposals of benign users to prevent their proposals from being created. Moreover, the only way to close the malicious proposal without completing it is to wait for it to expire (15 days long). The attacker only needs to front-run benign DAO proposals every 15 days to effectively perform a DoS attack on the DAO.
Referenced code: Dao.sol#L310 Dao.sol#L319 Dao.sol#L329 Dao.sol#L339 Dao.sol#L352 Dao.sol#L407
Add a function that allows the deployer to remove malicious proposals directly in case this attack happens.
#0 - SamusElderg
2021-07-25T11:36:30Z
Duplicate of #43
shw
Some critical parameters (e.g., the maximum number of pools that can be curated) of the Spartan protocol are hard-coded in the contracts and thus unchangeable after the contracts are deployed. Allowing admins to adjust such parameters dynamically makes the code easier to maintain and adds flexibility to the protocol without re-deploying the related contracts.
Referenced code: Dao.sol#L407 DaoVault.sol#L69 poolFactory.sol#L31
Consider adding a function with the onlyDAO
modifier to allow the deployer or DAO to change these parameters in case of need.
#0 - SamusElderg
2021-07-25T11:54:50Z
Duplicate of #172
532.1708 USDC - $532.17
shw
The claimAllForMember
function of Dao
is permissionless, allowing anyone to claim the unlocked bonded LP tokens for any member. However, claiming a member's LP tokens could decrease the member's weight in the BondVault
, thus affecting the member's votes and rewards in the Dao
contract.
For example, an attacker can intentionally front-run a victim's voteProposal
call to decrease the victim's vote weight to prevent the proposal from being finalized:
BondVault
is 201, the total weight is 300. The victim has some LP tokens claimable from the vault, and if claimed, the victim's weight will be decreased to 101. To simplify the situation, assuming that the victim's weight in the DaoVault
and the total weight of the DaoVault
are both 0.voteProposal
, the proposal will be finalized since the victim has the majority weight (201/300 > 66.6%).claimAllForMember
with the victim as the parameter to intentionally decrease the victim's weight.Similarly, an attacker can front-run a victim's harvest
call to intentionally decrease the victim's reward since the amount of reward is calculated based on the victim's current weight.
Referenced code: Dao.sol#L179-L206 Dao.sol#L276-L285 Dao.sol#L369-L383 Dao.sol#L568-L574 Dao.sol#L577-L586 BondVault.sol#L104-L117 BondVault.sol#L120-L129 BondVault.sol#L155-L162
Consider removing the member
parameter in the claimAllForMember
function and replace all member
to msg.sender
to allow only the user himself to claim unlocked bonded LP tokens.
#0 - verifyfirst
2021-07-26T00:50:27Z
Although a low risk issue, it is valid and the suggested mitigation is correctly proposed.
#1 - ghoul-sol
2021-08-08T21:28:35Z
Making this medium risk as no funds are lost.
shw
Unbounded loops, for example, iterating over a variable-length array, could cost an arbitrarily amount of gas. If the required gas of executing the loop exceeds the block gas limit, the transactions will revert, causing some critical functions to be disabled.
For example, the claimAllForMember
function of Dao
iterates the listedBondAssets
to claim all unlocked bonded LP tokens of a member. However, the listedBondAssets
array contains the historical array of all past bond listed assets, whose length continuously increases over time. Thus, if the array length is too large, this function could cost gas more than the block gas limit. Even not, users calling this function could suffer from large gas consumption. See the following links for all unbounded loops.
Referenced code: poolFactory.sol#L100-L104 synthVault.sol#L122-L128 Dao.sol#L278-L283
Consider allowing users to decide the start and end indices when iterating the loop to avoid iterating the whole array. For example, re-write the claimAllForMember
as follows:
function claimAllForMember(address member, uint start, uint end) external returns (bool) { for (uint i = start; i < end; i++) { uint claimA = calcClaimBondedLP(member, listedBondAssets[i]); // Check user's unlocked Bonded LPs for each asset if (claimA > 0) { _BONDVAULT.claimForMember(listedBondAssets[i], member); // Claim LPs if any unlocked } } return true; }
#0 - SamusElderg
2021-07-25T12:02:36Z
Duplicate of #37
#1 - ghoul-sol
2021-08-08T19:54:41Z
Duplicate of #37 so low risk
shw
The _handleTransferIn
function of PoolFactory
does not correctly handle the case where the provided parameter _token
is address(0)
, causing the createPoolADD
function to revert when the token is provided as BNB.
Referenced code: poolFactory.sol#L109-L115 Router.sol#L197-L211
Change the _handleTransferIn
implementation of PoolFactory
to that of Router
.
#0 - SamusElderg
2021-07-25T11:52:42Z
Duplicate of #7
🌟 Selected for report: shw
394.2006 USDC - $394.20
shw
Several functions in Utils
do not handle edge cases where the divisor is 0, caused mainly by no liquidity in the pool. In such cases, the transactions revert without returning a proper error message.
Referenced code: Utils.sol#L75 Utils.sol#L90 Utils.sol#L109-L110 Utils.sol#L123-L124 Utils.sol#L131 Utils.sol#L138 Utils.sol#L155 Utils.sol#L189 Utils.sol#L195 Utils.sol#L215
Check if the divisors are 0 in the above functions to handle edge cases.