- Published on
#6 Solo Review: Reaper Strategy - Options Token Compounder
- Authors
- Name
- Beirao
- @0xBeirao
Introduction
A time-boxed security review of the Options Token Compounder protocol was done by Beirao, with a focus on the security aspects of the application's smart contracts implementation.
Disclaimer
A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or even if the review will find any problems with your smart contracts. Subsequent security reviews, bug bounty programs and on-chain monitoring are strongly recommended.
About Beirao
I’m an independent smart contract security researcher. I extend my skills as a contractor specializing in EVM smart contracts security. If you're in need of robust code review, I'm here to help. We can get in touch via Twitter or Email.
About Options Token Compounder
The OptionsCompounder contract is an abstract token that should be implemented into a complete strategy. The OptionsCompounder helps to compound oTokens by exercising them at a discounted price and then exchanging the payment token for the vault asset (want). The want token can then be used in a strategy.
Flow of Options Token Compounder used in a strategy (from the documentation)
The strategy must properly implement the virtual functions of the Options Token Compounder it inherits from
- Admin configures swap paths, oracles, initializer args, etc
- Strategy has an oToken balance
- Keeper calls harvestOTokens
- getPaymentAmount from discountExercise given oToken balance
- Flash loan the necessary amount of funds to exercise in paymentToken
- Flashloan callback -> executeOperation -> exerciseOptionAndReturnDebt
- oTokens are exercised using paymentToken that was flash loaned
- Underlying token is received by the strategy, swap entire amount into payment token to repay flashloan
- Calculate minAmountOut by directly querying the same oracle consumed by the DiscountExercise we interact with
- Assess profitability in units of paymentToken, swap profits to want of the strategy if not same token as paymentToken
- Calculate minAmountOut by directly querying the same oracle used by the DiscountExercise we interact with
- Assess profitability in units of wantToken
- Emit event reflecting the oTokens compounded
Observations
If we forget about admin/config functions. The operational flow consists of
harvestOTokens()(limited to the KEEPER role)
This OptionsCompounder takes place in a larger ecosystem of contracts, that are composed by:
- the
reaperVaultV2, which acts like an ERC4626 vault - and all the strategies implemented in that vault to generate returns.
The OptionsCompounder seems to fit nicely into this system. It doesn't interfere with the overall system.
Privileged Roles & Actors
The OptionsCompounder borrows the ReaperBaseStrategyv4 access control which consists of 4 roles but only 2 are used in this contract:
- ADMIN - can configure the contract
- KEEPER - can call
harvestOTokens().
Severity classification
| Severity | Impact: High | Impact: Medium | Impact: Low |
|---|---|---|---|
| Likelihood: High | High | High | Medium |
| Likelihood: Medium | High | Medium | Low |
| Likelihood: Low | Medium | Low | Low |
Impact - the technical, economic and reputation damage of a successful attack
Likelihood - the chance that a particular vulnerability gets discovered and exploited
Severity - the overall criticality of the risk
Security Assessment Summary
review commit hash - 4111464a
fixes review commit hash - 35177bf7
Scope
The following smart contracts were in scope of the audit: (total : 319 SLoC)
OptionsCompounder.sol
Findings Summary
Summary :
- 1 High
- 2 Lows
- 2 Improvementss
| ID | Title | Status |
|---|---|---|
| [H-01] | Loss of fund when the Exercise contract is not funded | fix |
| [L-01] | lendingPool and addressProvider can't be changed | fix |
| [L-02] | Initial balance of PaymentToken will never be swapped | fix |
Detailed Findings
[H-01] Loss of fund when the Exercise contract is not funded
Description
The Option Token contract has been modified to delay payment if the Exercise contract is not sufficiently funded, but this strategy has not been modified to deal with this new mechanism.
If the Exercise contract is not sufficiently funded, the underlyingToken is not sent (DiscountExercise.sol#L177) and the [claim()](https://github.com/Byte-Masons/options-token/blob/9aa8a91e6788ada2354682e43e781e1c691af574/src/exercise/DiscountExercise.sol#L105-L110) function must be called later.
Since OptionsCompounder never calls this claim function, any delayed funds will be lost.
Recommendations
I recommend to revert if the Exercise contract is not enough funded.
Add this line before calling optionToken.exercise() (OptionsCompounder.sol#L318)
require(underlyingToken.balanceOf(exerciserContract) >= optionsAmount);
You can also choose to deal with this delayed payment mechanism, but this will require more modifications.
[L-01] lendingPool and addressProvider can't be changed
Description
This can be critical if the lendingPool is paused or hacked, the flashloan will be inaccessible causing a DOS of the compounding feature.
Recommendations
You can do a full upgrade to fix this, but I recommend adding a setter.
[L-02] Initial balance of PaymentToken will never be swapped
Description
To calculate the amount of paymentToken to swap, we subtract the initialBalance:
gainInPaymentToken = (assetBalance - initialBalance) - totalAmountToPay;
This should not happen, but if there are paymentTokens left in the contract, they will not be swapped.
POC
- KEEPER calls
harvestOTokens()but forgets to pass aminWantAmount(sominWantAmount== 0). - The swap fails (OptionsCompounder.sol#L366) but since the swapper (swap is in a try/catch) doesn't revert the transaction continues.
- This last slippage check pass since
gainInWantToken == minWantAmountOptionsCompounder.sol#L378 - Now
paymentTokenare left in the contract and can be swapped (without admin intervention)
Recommendations
- Do a 0 check on
minWantAmount. - You can also remove the initialBalance subtraction OptionsCompounder.sol#L352
gainInPaymentToken = assetBalance - totalAmountToPay;
[I-01] The strategy can only deal with one exercise contract
oToken can work with multiple Exercise contracts, but OptionsCompounder can only work with one.
If you think that multiple exercise contracts can be a use case, you can add this support.
[I-02] modes[] wrong array size
OptionsCompounder.sol#L212 should be 1 not 2.