Submitted on Mar 7th 2024 at 02:56:41 UTC by @kaysoft for Boost | Puffer Finance
Report ID: #29099
Report type: Smart Contract
Report severity: Insight
Target: https://etherscan.io/address/0x7276925e42f9c4054afa2fad80fa79520c453d6a
Impacts:
- Contract fails to deliver promised returns, but doesn't lose value
Due to stETH's 1-2 corner case the actual amount transfered from user is when the depositStETH()
function is less than the specified amount in the parameter.
In the function below, permitData.amount
of stETH
is transfered from the msg.sender
to the the PufferDepositor
contract. The issue is that the actual amount transferred from msg.sender to the contract is less than the specified permitData.amount
.
The reason for this is described in 1-2 wei stETH corner case
File: PufferDeposit.sol
function depositStETH(Permit calldata permitData) external restricted returns (uint256 pufETHAmount) {
try ERC20Permit(address(_ST_ETH)).permit({
owner: msg.sender,
spender: address(this),
value: permitData.amount,
deadline: permitData.deadline,
v: permitData.v,
s: permitData.s,
r: permitData.r
}) { } catch { }
192: SafeERC20.safeTransferFrom(IERC20(address(_ST_ETH)), msg.sender, address(this), permitData.amount);
return PUFFER_VAULT.deposit(permitData.amount, msg.sender);//@audit use transfershares: https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case
}
For example is you specify permitData.amount
as 1000000000000000000001
the actual amount that will be pulled from the user is 1000000000000000000000
.
This leaves 1 wei of stETH that is not pulled from msg.sender.
Why this is an issue is that there are 2 transfers:
- First from msg.sender to the
PufferDepositor
contract - And secondly from the
PufferDepositor
to the PufferVault contract.
1 wei of stETH difference may seem small but the doc further stated that:
The same thing can actually happen at any transfer or deposit transaction. >In the future, when the stETH/share rate will be greater, the error can >become a bit bigger.
Possible Denial of service due to transfer amount difference.
Short Term:
Consider using the transferShares()
function to transfer stETH as recommended by Lido docs
Long Term:
Instead of stEth
, consider integration the non-rebasable value-accruing counterpart wstETH
as recommeded by the Lido docs
https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case
- Create a file in the
test/integration
directory and name isPOC.t.sol
- Copy and paste the code below to the new file:
POC.t.sol
. - Run
forge test --match-path test/Integration/POC.t.sol -vvv
- The test should fail with some
logs
of amounts - This fails because
depositAmount
is not equal toactualAmount
.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
import { TestHelper } from "../TestHelper.sol";
import { Permit } from "../../src/structs/Permit.sol";
import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol";
import "forge-std/console.sol";
contract PufferDepositorV2ForkTest is TestHelper {
function test_stETH_deposit_pulls_less_stETH()
public
giveToken(BLAST_DEPOSIT, address(stETH), alice, 35000 ether)
withCaller(alice)
{
uint256 depositAmount = 1000000000000000000001;
Permit memory permit = _signPermit(
_testTemps(
"alice",
address(pufferDepositor),
depositAmount,
block.timestamp,
hex"260e7e1a220ea89b9454cbcdc1fcc44087325df199a3986e560d75db18b2e253"
)
);
uint256 balanceBefore = stETH.balanceOf(alice);
pufferDepositor.depositStETH(permit, alice);
uint256 balanceAfter = stETH.balanceOf(alice);
console.log("Alice's Deposit Amount:::", depositAmount);
uint256 actualTransfer = balanceBefore - balanceAfter;
console.log("Actual stETH transfered from Alice:::", actualTransfer );
console.log("Amount of stETh not transfered:::", depositAmount - actualTransfer);
// This will revert since not all depositAmount was transfered from Alice.
// assertEq(actualTransfer, depositAmount, "despositAmount and actual transfer should be same");
}
}