The LibConvert
uses the block.number
to prevent flash loan attacks that can cause price manipulation.
I asked the devs why they decided to use block.number
instead of block.timestamp
. Here's the reply from @pizzaman1337:
Using block number seemed cleaner than timestamp to me, since the idea was to limit converts on a per-block basis.
The problem with this approach is the following:
Beanstalk determines the "conversion capacity" based on the capped reserves.
LibPipelineConvert reads the overall cappedDeltaB and passes it into the overallConvertCapacity
-> prepareStalkPenaltyCalculation
which passes it to LibConvert.applyStalkPenalty
. Look at the snippet code below:
Finally, LibConvert -> applyStalkPenalty
and LibConvert -> calculateConvertCapacityPenalty
use this value to determine the overall convert capacity and the penalty fee:
The readCappedReserves(MultiflowPump v1.1.0) works in the following manner:
Updates the reserves if the delta time is > 0.
Return the the last capped reserves if delta time == 0 or it caps the current reserves based on the data defined for the ConstantProduct2.
MultiflowPump -> readCappedReserves:
As shown above, Basin works with block. timestamp
to return the capped reserves and avoid price manipulation in the same block. But Beanstalk is working with block.number
. Due to this approach, Beanstalk is now exposed to the following issues when deployed on L2:
L2s batches transactions into single L1 blocks, thus multiple L2 blocks can share the same L1 block number. It means that some L2s like Arbitrum, the value of block.number
can repeat for several blocks.
References:
Arbitrum
In general, the block.number
on L2s is not a reliable source of timing information and the time between each block is also different from Ethereum. This is because each transaction on L2 is placed in a separate block and blocks are not produced at a constant rate.
References:
OZ audit report,
C4 Audit
Users may incur penalty fees for conversions due to executing their transactions in a block where the block.number has already reached its full capacity for conversions.
Users are vulnerable to being front-run by bots that exploit the full conversion capacity of a block. Bots can execute conversions first, using up the block’s capacity, and causing subsequent user transactions to incur penalty fees, thus burning their Grown Stalk unfairly.
Manual Review & Foundry
Replace block.number
with block.timestamp
to ensure accurate timing and prevent issues on L2s.
For Arbitrum, use ArbSys(100).arbBlockNumber()
to obtain a unique Arbitrum-specific block number.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.