A four person-day audit was performed on KeeperDAO's protocol. In total, 2 undetermined, 2 high, 2 low, and 5 informational findings were documented. Additionally, 7 recommendations were made.
Contracts (excluding mocks) located within the following repositories at the specified commit hash were included in the audit.
| Repository | Commit Hash |
|---|---|
| keeperdao/protocol | 8edbc65a2227ecb49b1ac0d62adc239f67dc4bcd |
| keeperdao/token | 0b659e993d844e68e85f7655f9a8e4157371e5f6 |
| keeperdao/liquiditypool | 72c3ccabde796780a7fd0f3e15d756561842ed25 |
| keeperdao/keeper | 2f9c043a759594da348adaba776f64056aea3da6 |
| keeperdao/compound | 0e709ac08c9da7349d233e578058b5ba50dd5295 |
| keeperdao/rebalancer | d1eb2c229d316edd23f57039240be2240f48bd80 |
| keeperdao/feecollector | f91c98390cecb60fda7356d0b3ffbdf472836e00 |
The risk of these findings are unquantifiable, but may be significant enough to warrant attention.
- Solidity optimizer is enabled
- Solidity versions are mismatched and out of date
These findings could significantly affect the operation of the project, do not require elevated privileges, and cannot be mitigated after deployment.
KyberNetworkRebalancercauses liquidations at high gas prices to fail- Attackers can steal profits from
KyberNetworkRebalancer
These findings could affect the operation of the project, may require elevated privileges, and cannot be mitigated after deployment.
- Functions are missing return statements
- Destroying a
SingleAssetLiquidityPooldoes not appear to behave correctly
These findings could affect the operation of the project, require elevated privileges, and may be mitigated after deployment.
KyberNetworkRebalancerwill fail to consolidate if fee or profit is zeroKTokenbalance is not tied to token ownership inKeeper- Entering and exiting Compound markets may fail
- Owners are able to change dangerous settings
- Functions may be vulnerable to reentrancy
These findings do not represent risk to the project, but the project may still benefit from addressing them.
- Consider exposing
recoverableTokensBlacklistinCanReclaimTokens - Consider emitting an event when a token is blacklisted
- Consider optimizing gas usage during consolidation
- Consider correcting typos
- Consider using wrapped ethereum instead of ethereum
- Consider using
SafeMathin additional locations - Consider applying the checks-effects-interactions pattern in additional locations
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0001 | Undetermined |
Currently, the Solidity optimizer is enabled for all repositories with the default runs value of 200.
The optimizer may introduce unexpected bugs which can only be detected through rigorous testing, formal verification, manual inspection of the generated bytecode, or some other technique. This unquantifiable risk may be undesirable depending on the exact magnitude of gas saved.
However, if the risk is considered to be acceptable, then it should be noted that the runs setting
does not necessarily specify the number of passes that the optimizer will make. Rather, it is defined
as the number of times that each instruction is expected to be executed, or in other words, the number
of expected transactions.
If the gas saved is not significant, consider disabling the Solidity optimizer. Alternatively,
if optimization is desired, consider changing the value of the runs setting to more
accurately reflect the expected number of transactions.
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0002 | Undetermined |
The Solidity versions used by each repository are listed below:
| Repository | Version |
|---|---|
| keeperdao/protocol | 0.5.8 |
| keeperdao/token | 0.5.8 |
| keeperdao/liquiditypool | 0.5.8 |
| keeperdao/keeper | 0.5.12 |
| keeperdao/compound | 0.5.12 |
| keeperdao/rebalancer | 0.5.8 |
| keeperdao/feecollector | 0.5.8 |
While recently released versions of Solidity have not been battle-tested and may contain
bugs, 0.5.15 was released in December and contains several bugfixes. Furthermore, while the
current codebase doesn't appear to be affected by these bugs, that may not be the case with
future upgrades. Finally, using multiple versions of Solidity exposes the protocol to more
risk than using a single version.
Consider upgrading all repositories to use Solidity version 0.5.15, or standardizing
on a single version of Solidity.
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0003 | High |
The KyberNetwork contract requires that the gas price of any transaction
interacting with it be less than a configurable value, currently set to 50 gwei:
function trade(TradeInput tradeInput) internal returns(uint) {
require(isEnabled);
require(tx.gasprice <= maxGasPriceValue);Kyber most likely introduced this limitation to protect their traders from sandwich attacks, where an attacker sends two transactions which sandwich the trader's transaction and make it less profitable.
However, KeeperDAO presumably would not want to be constrained in terms of gas price. Liquidation is competitive, with bots bidding by increasing the gas price of their transaction. A gas price cap imposed by Kyber would artificially cripple KeeperDAO's ability to keep up with the competition, directly impacting profits.
It may be possible to negotiate with the Kyber team to have KeeperDAO be
exempt from the gas price limitations. Failing that, simply
documenting this limitation or introducing a Rebalancer which can
rebalance collateral on a different exchange when gas prices are too
high may be an alternative.
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0004 | High |
Recent events have shown that attacks which profit from slippage can be rather
lucrative. The KyberNetworkRebalancer is vulnerable to a form of this attack
during consolidation of leftover collateral (i.e. profit)
An attacker can write a smart contract which uses flash loans to depress the
exchange rate between _leftoverToken and assetToken, before triggering
a consolidation on the KyberNetworkRebalancer. This will allow the attacker
to essentially extract the profits from the KyberNetworkRebalancer for themselves.
The KeeperDAO team could choose to restrict consolidation to EOA addresses, or to simply make consolidation a privileged action. Alternatively, the KeeperDAO team could choose to run a bot which attempts to automatically consolidate funds, although that bot is still at risk of being frontrun. Finally, the KeeperDAO team could simply accept this as a known limitation of the protocol.
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0005 | Low |
The following functions are missing return statements.
Compound#unbalancedBalanceCompound#takeCompound#withdrawLiquidityPool#withdraw
Ensure that the functions are returning a value, or remove the returns clause from the function definition.
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0006 | Low |
When a SingleAssetLiquidityPool is destroyed, it tries to transfer its position as operator on the Keeper to the sender.
However, only the owner has the ability to do this. Therefore, one of two outcomes is possible:
- The call will fail because the
SingleAssetLiquidityPoolis not the owner of theKeeper - The call will succeed and ownership of the
Keeperis lost forever
Neither of these outcomes appear to be ideal.
Allow operators to transfer operator permissions on SingleLenderKeeper, or transfer ownership
after transferring operator permissions to ensure ownership is not burnt.
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0007 | Informational |
When consolidating leftover collateral, the KyberNetworkRebalancer will consult
the FeeCollector instance for the fee to charge. If this fee is either zero, or
the entirety of the collateral, then the entire transaction will revert.
This is because Kyber does not support performing swaps where the src token amount is zero:
function validateTradeInput(ERC20 src, uint srcAmount, ERC20 dest, address destAddress)
internal
view
returns(bool)
{
require(srcAmount <= MAX_QTY);
require(srcAmount != 0);
require(destAddress != address(0));
require(src != dest);Check if the value to be exchanged is zero, and don't perform the exchange if it is the case.
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0008 | Informational |
Suppose there currently exists some Lender instance which contains some funds, as well as a corresponding Keeper.
Now suppose that a new LiquidityPool is deployed and paired with the existing Keeper.
A user deposits some funds via the new LiquidityPool. Because the total supply of the KToken is zero, the user is minted
TOKEN_MULTIPLIER * _amount tokens.
The user then immediately withdraws the tokens they were just minted. First, the scaled ratio must be calculated.
uint256 scaledRatio = _state.keeper.ratioScale().mul(_amountOfKTokens).div(_state.kToken.totalSupply());
// scaledRatio = _state.keeper.ratioScale()
Now, the withdraw amount must be calculated.
function valueOf(address _token, uint256 _scaledRatio) external returns (uint256) {
return lender.balanceOf(_token).mul(_scaledRatio).div(state.ratioScale);
}
uint256 withdrawAmount = _state.keeper.valueOf(address(_state.assetToken), scaledRatio);
// withdrawAmount = lender.balanceOf(address(_state.assetToken))
We can see that the user will end up withdrawing the entire balance of the Lender, despite not having deposited
that amount. This is because the number of tokens minted is not based on the value of the user's deposit relative to the Lender's balance.
Disallow the changing of operators on a Keeper instance such that this cannot be exploited by an
administrator, or devise an alternative issuance algorithm which accounts for this situation
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0009 | Informational |
When entering a market on Compound, the operation may fail if the supplied CToken is unlisted or if
the sender has entered too many markets already.
Similarly, exiting a market may fail if the sender owes money in the market or would not have enough collateral after exiting the market.
While it's possible for the Compound lender to recover from any failed enters or exits, it would be safer to assert that the operation succeeded before making any state changes.
Wrap the calls to Comptroller#enterMarkets and Comptroller#exitMarket with requireNoError
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0010 | Informational |
Owners have the ability to change a variety of state variables, but some of the variables shouldn't need to be changed and makes the owner key riskier to hold.
The following fields could probably be made immutable:
| Field | Reasoning |
|---|---|
Compound#gastoken |
The GasToken contracts have existed for a while now, and there probably won't be a need to replace them |
SingleAssetLiquidityPool#assetToken |
Given that the pool should only hold a single asset, it seems like in all cases where the token needs to be replaced, it would be better to deploy a new pool instead |
SingleAssetLiquidityPool#kToken |
A new instance of KToken is created at deploy-time, there doesn't seem to be a need to ever change this |
Remove the ability for the owner to change these values
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0011 | Recommendation | KD-0017 |
Reentrancy can cause unpredictable behavior in otherwise sound contracts. Some contracts in the KeeperDAO protocol interact with external contracts whose implementations may allow for reentrancy.
The following functions may benefit from reentrancy protection:
| Function | Reasoning |
|---|---|
Compund#liquidate |
This function involves movement of protocol funds to an attacker-accessible location and triggers a transfer call on an ERC20 token. This call could potentially trigger a callback, if for example the token also implemented ERC777. |
SingleAssetLiquidityPool#withdraw |
This function involves movement of protocol funds to an attacker and triggers a transfer call on an ERC20 token |
Apply defense in depth and use OpenZeppelin's ReentrancyGuard.sol to prevent
the possibility of reentrancy attacks.
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0012 | Recommendation |
Users may be interested in being able to verify whether a token is blacklisted, either through a contract, or through the Etherscan "Read Contract" view.
Set the visibility of the field to public
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0013 | Recommendation |
Users may be interested in compiling a list of blacklisted tokens off-chain, but at the moment that involves non-trivial tracing of all transactions which may interact with the protocol.
Define and emit an event, such as event TokenBlacklisted(address indexed token)
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0014 | Recommendation |
The average Kyber exchange uses about 300,000 gas. By making one trade instead of two trades during consolidation, significant gas savings can be obtained.
Perform one swap for the entirety of leftoverCollateral and manually transfer the assetToken
to the feeCollector and state.keeper
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0015 | Recommendation |
The following files contain typos
| Files | Typo |
|---|---|
KToken.sol#L59-69 |
underlyingTokenName is misspelled as underlyngTokenName |
KToken.sol#L71-78 |
underlyingTokenSymbol is misspelled as underlyngTokenSymbol |
KyberNetworkRebalancer.sol#L27 |
contract is misspelled as cotract |
Rebalancer.sol#L12 |
converted is misspelled as conveted |
Correct typos
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0016 | Recommendation |
There's a non-trivial amount of code centered around handling ETH as a special case, and code complexity increases the risk of bugs.
Consider using WETH instead of ethereum. The ETH can be wrapped in entrypoints for the sake of user convenience and unwrapped in any lenders which don't accept WETH. This reduces complexity when flowing value from users to lenders. Note that Kyber already supports trading WETH
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0017 | Recommendation | KD-0011 |
The checks-effects-interactions pattern was developed to reduce the risk associated with reentrancy. While not a panacea, following the CEI pattern is usually better than not.
The following functions may benefit from following the CEI pattern:
| Function | Reasoning |
|---|---|
SingleAssetLiquidityPool#withdraw |
This function interacts with the trusted KToken after affecting external state by causing an ERC20 transfer |
Apply the CEI pattern where reasonable
| Finding ID | Severity | Related | Resolution |
|---|---|---|---|
| KD-0018 | Recommendation |
OpenZeppelin's SafeMath is a cornerstone of safe smart contract development.
Sometimes it's hard to guarantee that an operation will always be safe. In those
cases, it may be better to err on the side of using SafeMath.
Some operations within KeeperDAO could benefit from the usage of SafeMath:
| Line number | Explanation |
|---|---|
KyberNetworkRebalancer#L156 |
While the fee collector is a privileged contract, the caller has no guarantees that the returned value will be less than than the input value |
LiquidityPool#L64 |
The _amount variable is technically user-controlled, even if in practice it's very unlikely this operation may overflow |
Use SafeMath in the locations indicated