Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions mainnet-contracts/src/PufferProtocol.sol
Original file line number Diff line number Diff line change
Expand Up @@ -323,13 +323,19 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad
// Get the burnAmount for the withdrawal at the current exchange rate
uint256 burnAmount =
_getBondBurnAmount({ validatorInfo: validatorInfos[i], validatorBondAmount: bondAmount });
// Failsafe in extreme rare case validator balance falls under (32-bondAmount) ETH
if (burnAmount > bondAmount) {
burnAmount = bondAmount; // residual loss socialized to pufETH holders
}

uint256 vtBurnAmount = _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i]);

// Update the burnAmounts
burnAmounts.pufETH += burnAmount;
burnAmounts.vt += vtBurnAmount;

// Store the withdrawal amount for that node operator
// Underflow is not possible because of the checks in the `_getBondBurnAmount` function and the fact that we are capping the burn amount to the bond amount
// nosemgrep basic-arithmetic-underflow
bondWithdrawals[i].pufETHAmount = (bondAmount - burnAmount);

Expand Down
29 changes: 29 additions & 0 deletions mainnet-contracts/test/unit/PufferProtocol.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,35 @@ contract PufferProtocolTest is UnitTestHelper {
assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 1 ether, 1, "bob got back the bond");
}

function test_batch_claim_underflow() public {
_registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice);

StoppedValidatorInfo memory aliceInfo = StoppedValidatorInfo({
module: NoRestakingModule,
moduleName: PUFFER_MODULE_0,
pufferModuleIndex: 0,
withdrawalAmount: 29 ether, // Withdrawal amount is less than 32 ETH - bond, but still shouldn't revert due to fix
startEpoch: 100,
endEpoch: _getEpochNumber(28 days, 100),
wasSlashed: false
});

StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](1);
stopInfos[0] = aliceInfo;

uint256 burnedAmount = pufferVault.convertToShares(1 ether); // 1 ETH to pufETH. This is the bond, the max that can be burned

vm.expectEmit(true, true, true, true);
emit IPufferProtocol.ValidatorExited(
_getPubKey(bytes32("alice")),
0,
PUFFER_MODULE_0,
burnedAmount,
_getVTBurnAmount(100, _getEpochNumber(28 days, 100))
);
pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos));
}

// Batch claim of different amounts
function test_different_amounts_batch_claim() public {
// Buy and approve VT
Expand Down
6 changes: 5 additions & 1 deletion mainnet-contracts/test/unit/Timelock.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ contract TimelockTest is Test {
stETHMock public stETH;
Timelock public timelock;

address public constant BROADCASTER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;

function setUp() public {
PufferDeployment memory deployment = new DeployPufETH().run();

Expand All @@ -33,6 +35,7 @@ contract TimelockTest is Test {
vm.assume(caller != timelock.OPERATIONS_MULTISIG());
vm.assume(caller != address(timelock));
vm.assume(caller != address(accessManager));
vm.assume(caller != BROADCASTER);

// Upgrades are forbidden
(bool canCall, uint32 delay) =
Expand Down Expand Up @@ -236,10 +239,11 @@ contract TimelockTest is Test {
assertTrue(!canCall, "should not be able to call");
}

function test_pause_depositor_slectors(address caller) public {
function test_pause_depositor_selectors(address caller) public {
vm.startPrank(timelock.pauserMultisig());
vm.assume(caller != address(timelock));
vm.assume(caller != address(accessManager));
vm.assume(caller != BROADCASTER);

address[] memory targets = new address[](1);
targets[0] = address(pufferDepositor);
Expand Down