Platform: Code4rena
Start Date: 27/01/2022
Pot Size: $90,000 USDC
Total HM: 21
Participants: 33
Period: 7 days
Judge: Jack the Pug
Total Solo HM: 14
Id: 78
League: ETH
Rank: 1/33
Findings: 8
Award: $19,451.47
🌟 Selected for report: 6
🚀 Solo Findings: 3
🌟 Selected for report: shw
Also found by: kirk-baird, pauliax
shw
Lack of access control on the assertGovernanceApproved
function of FlashGovernanceArbiter
allows anyone to lock other users' funds in the contract as long as the users have approved the contract to transfer flashGovernanceConfig.amount
of flashGovernanceConfig.asset
from them.
approve
on the flashGovernanceConfig.asset
to allow FlashGovernanceArbiter
to transfer flashGovernanceConfig.amount
of assets from her.approve
transaction and decides to front-run it. He calls assertGovernanceApproved
with sender
being Alice, target
being any address, and emergency
being true
.flashGovernanceConfig.unlockTime
period.Referenced code: DAO/FlashGovernanceArbiter.sol#L60-L81
Only allow certain addresses to call the assertGovernanceApproved
function on FlashGovernanceArbiter
.
#0 - gititGoro
2022-02-10T00:38:26Z
duplicate of 268
#1 - jack-the-pug
2022-02-16T08:37:32Z
Upgrading to high
as the users' funds are at risk.
#2 - gititGoro
2022-03-02T03:01:49Z
The reason I stuck with medium risk is because the user's funds can't be lost in this scenario. Only temporarily locked. If the user unapproves FlashGovernanceArbiter on EYE then they simply have to wait until the unlock period has passed and can withdraw again.
#3 - jack-the-pug
2022-03-02T04:33:22Z
The reason I stuck with medium risk is because the user's funds can't be lost in this scenario. Only temporarily locked. If the user unapproves FlashGovernanceArbiter on EYE then they simply have to wait until the unlock period has passed and can withdraw again.
Agreed. This should be somewhere in between Med and High. If it's just the users' deposits being temporarily locked, then it's definitely a Med. But this one is taking probably all the funds from users' wallets and locking them against their will, easy to pull off by anyone, all at once for all potential victims.
I tend to make it a High so that the future wardens and probably by extent the devs can be more careful with allowances. We have seen so many incidents cause by improper handling of users' allowances.
A transferFrom()
with from not being hard-coded as msg.sender
is evil.
My fellow wardens, if you are reading this, do not go easy on a transferFrom()
that takes an argument as from
.
#4 - gititGoro
2022-05-30T04:10:01Z
Fixed with https://github.com/Behodler/limbo/pull/16
shw
The implementation of the transferAndCall
function in ERC677
is incorrect. It transfers the _value
amount of tokens twice instead of once. Since the Flan
contract inherits ERC667
, anyone calling the transferAndCall
function on Flan
is affected by this double-transfer bug.
Below is the implementation of transferAndCall
:
function transferAndCall( address _to, uint256 _value, bytes memory _data ) public returns (bool success) { super.transfer(_to, _value); _transfer(msg.sender, _to, _value); if (isContract(_to)) { contractFallback(_to, _value, _data); } return true; }
We can see that super.transfer(_to, _value);
and _transfer(msg.sender, _to, _value);
are doing the same thing - transfering _value
of tokens from msg.sender
to _to
.
Referenced code: ERC677/ERC677.sol#L28-L29
Remove _transfer(msg.sender, _to, _value);
in the transferAndCall
function.
#0 - gititGoro
2022-02-10T00:03:37Z
duplicate of issue 1
#1 - jack-the-pug
2022-02-16T09:02:04Z
Dup #1
#2 - jack-the-pug
2022-02-16T09:02:30Z
Making this the main issue
#3 - gititGoro
2022-05-29T04:33:50Z
🌟 Selected for report: shw
6626.1986 USDC - $6,626.20
shw
A logic error in the burnFlashGovernanceAsset
function that resets a user's pendingFlashDecision
allows that user to steal other user's assets locked in future flash governance decisions. As a result, attackers can get their funds back even if they execute a malicious flash decision and the community burns their assets.
FlashGovernanceArbiter
contract.burnFlashGovernanceAsset
to burn her locked assets. However, the burnFlashGovernanceAsset
function resets Alice's pendingFlashDecision
to the default config (see line 134).withdrawGovernanceAsset
to withdraw Bob's locked asset, effectively the same as stealing Bob's assets. Since Alice's pendingFlashDecision
is reset to the default, the unlockTime < block.timestamp
condition is fulfilled, and the withdrawal succeeds.Referenced code: DAO/FlashGovernanceArbiter.sol#L134 DAO/FlashGovernanceArbiter.sol#L146
Change line 134 to delete pendingFlashDecision[targetContract][user]
instead of setting the pendingFlashDecision
to the default.
#0 - gititGoro
2022-05-29T01:46:14Z
🌟 Selected for report: shw
6626.1986 USDC - $6,626.20
shw
The LP pricing formula used in the burnAsset
function of LimboDAO
is vulnerable to flashloan manipulation. By swapping a large number of EYE into the underlying pool, an attacker can intentionally inflate the value of the LP tokens to get more fate
than he is supposed to with a relatively low cost.
With the large portion of fate
he gets, he has more voting power to influence the system's decisions, or even he can convert his fate
to Flan tokens for a direct profit.
Below is an example of how the attack works:
1000 * 100/1000 * 20 = 2000
amount of fate
.x * y = k
, ignoring fees for simplicity). Now the pool contains 2000 EYE and 500 LINK tokens.2000 * 100/1000 * 20 = 4000
amount of fate
.fate
by only paying the swapping fees to the pool. The more EYE tokens he swaps into the pool, the more fate
he can get. This attack is practically possible by leveraging flashloans or flashswaps from other pools containing EYE tokens.The setEYEBasedAssetStake
function has the same issue of using a manipulatable LP pricing formula. For more detailed explanations, please refer to the analysis of the Cheese Bank attack and the Warp Finance attack.
Referenced code: DAO/LimboDAO.sol#L356 DAO/LimboDAO.sol#L392
Use a fair pricing formula for the LP tokens, for example, the one proposed by Alpha Finance.
#0 - gititGoro
2022-02-11T03:16:01Z
This is actually a good fate inflation vector especially when combined with the fateToFlan conversion
#1 - jack-the-pug
2022-02-27T07:39:09Z
Good catch! A valid economic attack vector can potentially be exploited using flashloans.
#2 - gititGoro
2022-05-29T00:09:29Z
🌟 Selected for report: camden
Also found by: GeekyLumberjack, kirk-baird, shw
362.2874 USDC - $362.29
shw
The generateFLNQuote
is permissionless, meaning that anyone can call this function to update the latestFlanQuotes
variables. However, when a token migrates from Limbo
to Beholder
, Limbo
calls the stabilizeFlan
function on UniswapHelper
, which ensures block times between two latestFlanQuotes
should be at least VARS.minQuoteWaitDuration
apart.
Therefore, an attacker can continuously call the generateFLNQuote
every VARS.minQuoteWaitDuration
blocks to prevent tokens from migrating to Beholder
, which effectively causes a DoS attack on the system. Although the attacker may not gain direct financial profit by launching such an attack, he might have an incentive to do it if it would cause a reputation loss on the Beholder
protocol.
Besides, it might not be a good idea to let anyone update generateFLNQuote
since the person can update the quotes when the prices have an advantage to himself (e.g., a higher or lower SCX price than the average). It further leads to an issue where people with conflicting preferences compete for the chance of updating the quote, causing the quotes to be updated too frequently and never be "stablized".
Referenced code: UniswapHelper.sol#L138-L145 UniswapHelper.sol#L162 Limbo.sol#L234
Only allow authorized people or contracts to update the quotes.
#0 - gititGoro
2022-02-05T03:04:24Z
The mitigation steps are wrong.
#1 - gititGoro
2022-02-10T04:12:13Z
duplicate of issue 102
#2 - jack-the-pug
2022-02-27T09:21:09Z
Dup #102
🌟 Selected for report: shw
1987.8596 USDC - $1,987.86
shw
Most of the functions with a governanceApproved
modifier call flashGoverner.enforceTolerance
to ensure the provided parameters are restricted to some range of their original values. However, in the governanceApproved
modifier, flashGoverner.setEnforcement(true);
is called after the function body is executed, and thus the changed values are not restricted during the function execution.
An attacker can exploit this bug to change some critical parameters to arbitrary values by flash governance decisions. The effect will last until the community executes another proposal to correct the values. In the meanwhile, the attacker may make use of the corrupted values to launch an attack.
adjustSoul
function of Limbo
, and sets the fps
of a soul to an extremely large value.FlashGovernanceArbiter
contract.claimReward
to get his rewards on the corresponding soul (assume that he has staked some number of the token before). Because of the manipulated fps
, he gets a large number of Flan tokens as the reward.Referenced code: DAO/Governable.sol#L46-L57 Limbo.sol#L380-L381 Limbo.sol#L327-L329 Limbo.sol#L530 Limbo.sol#L628-L630
Rewrite the _governanceApproved
function and the governanceApproved
modifier as follows:
function _governanceApproved(bool emergency) internal { bool successfulProposal = LimboDAOLike(DAO).successfulProposal(msg.sender); if (successfulProposal) { flashGoverner.setEnforcement(false); } else if (configured) { flashGoverner.setEnforcement(true); flashGoverner.assertGovernanceApproved(msg.sender, address(this), emergency); } } modifier governanceApproved(bool emergency) { _governanceApproved(emergency); _; }
#0 - gititGoro
2022-02-05T03:14:01Z
So this is a vulnerability for the very first execution of flashgovernance decision on a contract, after which it's safe. This is the type of thing that won't be acted upon because it will have gone away by the time the public interacts with Limbo. However, it is technically true so I'm confirming the issue.
#1 - jack-the-pug
2022-02-27T08:06:50Z
Valid finding, but the conditions are quite strict, downgraded to Med
.
#2 - gititGoro
2022-04-09T02:35:52Z
FIX https://github.com/Behodler/limbo/pull/13
Note: an additional flash loan bug that wasn't picked up in the audit was fixed.
536.7221 USDC - $536.72
shw
Most of the proposal contracts have a parameterize
function for setting the proposal parameters, and these functions are protected only by the notCurrent
modifier. When the proposal is proposed through a lodgeProposal
transaction, an attacker can front-run it, modify the proposal parameters, and let the community vote it down. As a result, the person proposing loses his fate
deposit.
ProposalFactory
and is ready to be proposed.lodgeProposal
function of ProposalFactory
to propose her proposal.parameterize
function to change the parameters to undesirable ones.Referenced code: DAO/Proposals/BurnFlashStakeDeposit.sol#L25-L37 DAO/Proposals/SetAssetApprovalProposal.sol#L21-L24 DAO/Proposals/ToggleWhitelistProposalProposal.sol#L22-L28 DAO/Proposals/UpdateMultipleSoulConfigProposal.sol#L40-L61 DAO/Proposals/WithdrawERC20Proposal.sol#L26-L32 DAO/ProposalFactory.sol#L74-L78
Only allow the creator of the proposal to modify the parameters.
#0 - gititGoro
2022-02-10T03:44:58Z
duplicate of issue 71
#1 - gititGoro
2022-05-31T22:16:12Z
🌟 Selected for report: kirk-baird
Also found by: shw
shw
The execute
function of the WithdrawERC20Proposal
contract calls limbo.withdrawERC20
, which is, however, not implemented in Limbo
but only defined in the LimboLike
interface. Therefore, users cannot execute the WithdrawERC20Proposal
proposal because the function call to limbo.withdrawERC20
always reverts, and the fate
that voters spend in the voting process is wasted.
The withdrawERC20
function is not defined in the Limbo
contract. Limbo
inherits from Governable
, but Governable
does not define this function either.
Referenced code: DAO/Proposals/WithdrawERC20Proposal.sol#L35
Implement the withdrawERC20
function in the Limbo
contract.
#0 - gititGoro
2022-02-11T03:00:17Z
duplicate of 165
#1 - jack-the-pug
2022-02-16T08:29:00Z
Dup #165
17.3281 USDC - $17.33
shw
Some tokens that are not compliant with the ERC20 standard may return a false
from the transfer
or transferFrom
functions to indicate the transfer failure. Without checking the returned value, the caller may not notice the transfer failure, which causes internal accounting errors.
Some tokens even do not return a boolean value, for example, USDT. In this case, directly calling ERC20.transfer
would cause the transaction to revert because of the decoding error on the return value (OpenZeppelin's IERC20 interface expects a return boolean).
The following lines of code operates on tokens that can be arbitrary (and thus may not follow the ERC20 standard):
Limbo.sol#L216 UniswapHelper.sol#L230 FlanBackstop.sol#L88 FlanBackstop.sol#L95 DAO/LimboDAO.sol#L22 DAO/LimboDAO.sol#L24 DAO/LimboDAO.sol#L386
Use the SafeERC20
library implementation from OpenZeppelin and call safeTransfer
or safeTransferFrom
when transferring ERC20 tokens.
#0 - gititGoro
2022-02-09T23:42:03Z
duplicate of issue 43
#1 - jack-the-pug
2022-02-22T14:25:23Z
Dup #37