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.
KyberNetworkRebalancer
causes 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
SingleAssetLiquidityPool
does not appear to behave correctly
These findings could affect the operation of the project, require elevated privileges, and may be mitigated after deployment.
KyberNetworkRebalancer
will fail to consolidate if fee or profit is zeroKToken
balance 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
recoverableTokensBlacklist
inCanReclaimTokens
- 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
SafeMath
in 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#unbalancedBalance
Compound#take
Compound#withdraw
LiquidityPool#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
SingleAssetLiquidityPool
is not the owner of theKeeper
- The call will succeed and ownership of the
Keeper
is 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