Platform: Code4rena
Start Date: 21/04/2022
Pot Size: $100,000 USDC
Total HM: 18
Participants: 60
Period: 7 days
Judge: gzeon
Total Solo HM: 10
Id: 112
League: ETH
Rank: 6/60
Findings: 2
Award: $4,493.16
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: sseefried
3860.1163 USDC - $3,860.12
A Staker -- that has their top-up position removed after execute
is called by a Keeper -- can always cause the transaction to revert. They can do this by deploying a smart contract to the payer
address that has implemented a receive()
function that calls revert()
. The revert will be triggered by the following lines in execute
if (vars.removePosition) { gasBank.withdrawUnused(payer); }
This will consume some gas from the keeper while preventing them accruing any rewards for performing the top-up action.
I have implemented a PoC in a fork of the contest repo. The attacker's contract can be found here.
Manual inspection
To prevent this denial of service attack some way of blacklisting badly behaved Stakers should be added.
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x52, 0xDjango, 0xkatana, Dravee, Funen, Kenshin, Ruhum, StyxRave, Tadashi, TerrierLover, TrungOre, antonttc, berndartmueller, catchup, csanuragjain, defsec, dipp, fatherOfBlocks, hake, horsefacts, hubble, jayjonah8, joestakey, kebabsec, kenta, m4rio_eth, oyc_109, pauliax, peritoflores, rayn, remora, robee, securerodd, simon135, sorrynotsorry, sseefried, z3s
633.036 USDC - $633.04
removeStakerVault
function providedAt some point in the future a particular token may be found to be vulnerable to some kind of attack, or not behave in a reasonable manner. It should be possible to remove a Staker Vault in these circumstances. At present there is no way to remove a staker vault, only add them.
execute
but appears non-exploitableReentrancy is possible on line 727 of function execute
because ETH is transfered. An attacker can point the beneficiary
parameter to a smart contract with a receive()
function implemented which can call back into backd contracts.
It does not seem that this reentrancy can be exploited since no state changes occur after the transfer of ETH. However, I have limited experience as an auditor and thought it would be best to point it out.
safeApprove
in TopUpLibrary.sol
There are multiple uses of safeApprove
in TopAction.sol
. It has similar issues to approve
where careful transaction ordering by an attacker could lead to uses of the old and the new allowance.
OpenZeppelin have deprecated this function here. A deeper discussion of the issue can be found in this GitHub Issue.
Preparable.execute
can be called by anyoneA number of functions in the code base indirectly call Preparable.execute
but can be executed by anyone. I'll refer to this family of functions as execute*
. This appears pretty safe since calls to prepare
and reset
are all permissioned.
I have not found an attack vector but I worry that it might be possible for an attacker to selectively order several contract calls, interspersing them with calls to execute*
to gain some kind of advantage. The deadlines set by prepare
are publicly viewable so the following example is feasible:
Example:
execute*
The way to mitigate this problem would be to make execute*
functions permissioned. The permission could be less restricive that requiring GOVERNANCE
or CONTROLLER
permissions, but more restrictive than allowing anyone on the Ethereum network.
AddressProvider.addStakerVault
can only return true
or revertFunction AddressProvider.addStakerVault can only return true
or revert.
Why return a boolean value at all if false
can never be returned?
TopUpAction._calcExchangeAmount
The documentation for _calcExchangeAmount uses the term "underlying" but does not mention the parameter actionToken
. Perhaps the documentation could be clarified?
TopUpActionLibrary.lockFunds
needs documentationFunction lockFunds spans 32 lines but has no documentation. It's a complicated function and probably deserves some.
TopUpAction
It would be very helpful if structs RecordKey
, RecordMeta
, Record
and RecordMeta
had more detailed documentation, preferably on a field by field basis.
TopUpAction.register
The documentation for function register is inaccurate.
It reads:
/** * @notice Register a top up action. * @dev The `depositAmount` must be greater or equal to the `totalTopUpAmount` (which is denominated in `actionToken`). * @param account Account to be topped up (first 20 bytes will typically be the address). * @param depositAmount Amount of `depositToken` that will be locked. * @param protocol Protocol which holds position to be topped up. * @param record containing the data for the position to register */
It states that depositAmount
is "Amount of depositToken
that will be locked" but this is not completely accurate. The actual amount locked is in record.totalTopUpAmount
. The amount locked will be equal to record.totalTopAmount
(but denominated in the the deposit token).
This is demonstrated in your tests one test_registration.py:283
I would suggest changing the following
* @dev The `depositAmount` must be greater or equal to the `totalTopUpAmount` (which is denominated in `actionToken`).
to this
* @dev The `depositAmount`, once converted to units of `actionToken`, must be greater or equal to the `totalTopUpAmount` (which is denominated in `actionToken`).
execute
pays fees in action token by swapping funds denominated in position tokenTop-up amounts are denominated in the token of the position (which is not always the action token). While this makes complete sense, the design to pay the top-up fees by swapping the position token for action token seems strange. Why not choose a design where users of the TopUpAction
contract deposit both the position token and, separately, action token for the fees?
With the present design you must swap the token for Action Token to pay the fees to the Keeper which introduces issues with slippage. The swap to Action Token can fail if there as been too much slippage.
#0 - chase-manning
2022-04-28T10:04:11Z
I consider this report to be of particularly high quality