Skip to content

Commit fe4735c

Browse files
authored
Unstake: use call not transfer (#268)
Use call rather than transfer to ensure enough gas is passed to staker to unstake
1 parent 688f056 commit fe4735c

File tree

2 files changed

+21
-4
lines changed

2 files changed

+21
-4
lines changed

contracts/staking/StakeHolder.sol

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ contract StakeHolder is AccessControlEnumerableUpgradeable, UUPSUpgradeable {
3131
/// @notice Error: Attempting to unstake amount greater than the balance.
3232
error UnstakeAmountExceedsBalance(uint256 _amountToUnstake, uint256 _currentStake);
3333

34+
/// @notice Error: Unstake transfer failed.
35+
error UnstakeTransferFailed();
36+
3437
/// @notice Error: The sum of all amounts to distribute did not equal msg.value of the distribute transaction.
3538
error DistributionAmountsDoNotMatchTotal(uint256 _msgValue, uint256 _calculatedTotalDistribution);
3639

@@ -137,7 +140,21 @@ contract StakeHolder is AccessControlEnumerableUpgradeable, UUPSUpgradeable {
137140

138141
emit StakeRemoved(msg.sender, _amountToUnstake, newBalance);
139142

140-
payable(msg.sender).transfer(_amountToUnstake);
143+
// slither-disable-next-line low-level-calls
144+
(bool success, bytes memory returndata) = payable(msg.sender).call{value: _amountToUnstake}("");
145+
if (!success) {
146+
// Look for revert reason and bubble it up if present.
147+
// Revert reasons should contain an error selector, which is four bytes long.
148+
if (returndata.length >= 4) {
149+
// solhint-disable-next-line no-inline-assembly
150+
assembly {
151+
let returndata_size := mload(returndata)
152+
revert(add(32, returndata), returndata_size)
153+
}
154+
} else {
155+
revert UnstakeTransferFailed();
156+
}
157+
}
141158
}
142159

143160
/**

test/staking/StakeHolderOperational.t.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ contract StakeHolderOperationalTest is StakeHolderBaseTest {
128128

129129
attacker.stake(10 ether);
130130
// Attacker's reentracy attack will double the amount being unstaked.
131-
// The attack fails due to an out of gas exception.
132-
vm.expectRevert();
133-
attacker.unstake{gas: 10000000}(1 ether);
131+
// The attack fails due to attempting to withdraw more than balance (that is, 2 x 6 eth = 12)
132+
vm.expectRevert(abi.encodeWithSelector(StakeHolder.UnstakeAmountExceedsBalance.selector, 6000000000000000001, 4 ether));
133+
attacker.unstake{gas: 10000000}(6 ether);
134134
}
135135

136136
function testRestaking() public {

0 commit comments

Comments
 (0)