pool = _deployPool(address(feeToken));
_stakeFee(alice, 100 * ONE);
_stakeFee(bob, 100 * ONE);
_contributeBonusFee(carol, 100 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(PRODUCTION);
pool.flagOutcome(SURVIVED, false, address(0));
uint256 aliceReceived = ...;
function test_H1_FeeOnTransfer_SilentUnderpaymentOnClaim() public {
pool = _deployPool(address(feeToken));
_stakeFee(alice, 100 * ONE, feeToken);
assertEq(pool.eligibleStake(alice), 99 * ONE);
assertEq(pool.totalEligibleStake(), 99 * ONE);
assertEq(feeToken.balanceOf(address(pool)), 99 * ONE);
_stakeFee(bob, 100 * ONE, feeToken);
assertEq(pool.totalEligibleStake(), 198 * ONE);
assertEq(feeToken.balanceOf(address(pool)), 198 * ONE);
_contributeBonusFee(carol, 100 * ONE, feeToken);
assertEq(pool.totalBonus(), 99 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
uint256 aliceBefore = feeToken.balanceOf(alice);
vm.prank(alice);
pool.claimSurvived();
uint256 aliceReceived = feeToken.balanceOf(alice) - aliceBefore;
uint256 poolBalanceAfterAlice = feeToken.balanceOf(address(pool));
uint256 bobBefore = feeToken.balanceOf(bob);
vm.prank(bob);
pool.claimSurvived();
uint256 bobReceived = feeToken.balanceOf(bob) - bobBefore;
console.log("Alice actually received:", aliceReceived);
console.log("Pool balance after Alice:", poolBalanceAfterAlice);
console.log("Bob actually received:", bobReceived);
assertLt(aliceReceived, 150 * ONE, "Alice underpaid due to transfer fee");
}
function test_H1_FeeOnTransfer_WithdrawAlsoUnderpays() public {
pool = _deployPool(address(feeToken));
_stakeFee(alice, 100 * ONE, feeToken);
uint256 accountedStake = pool.eligibleStake(alice);
uint256 aliceBefore = feeToken.balanceOf(alice);
vm.prank(alice);
pool.withdraw();
uint256 aliceReceived = feeToken.balanceOf(alice) - aliceBefore;
assertLt(aliceReceived, accountedStake, "Withdraw underpays due to transfer fee");
assertEq(aliceReceived, accountedStake - (accountedStake / 100), "Exactly 1% fee lost");
}
function test_H1_NegativeRebasing_CausesInsolvency() public {
pool = _deployPool(address(standardToken));
_stake(alice, 100 * ONE, standardToken);
_stake(bob, 100 * ONE, standardToken);
_contributeBonus(carol, 50 * ONE, standardToken);
assertEq(standardToken.balanceOf(address(pool)), 250 * ONE);
vm.prank(address(pool));
standardToken.transfer(address(0xDEAD), 25 * ONE);
assertLt(standardToken.balanceOf(address(pool)), pool.totalEligibleStake() + pool.totalBonus());
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
vm.prank(alice);
pool.claimSurvived();
}
+ function _safeTransferWithAccounting(address to, uint256 accountedAmount) internal {
+ uint256 balanceBefore = stakeToken.balanceOf(address(this));
+ stakeToken.safeTransfer(to, accountedAmount);
+ uint256 balanceAfter = stakeToken.balanceOf(address(this));
+ uint256 actuallySent = balanceBefore - balanceAfter;
+ if (actuallySent < accountedAmount) {
+ // Token has transfer fees - revert or adjust accounting
+ revert TransferFeeDetected();
+ }
+ }
+
function withdraw() external nonReentrant {
// ...
- stakeToken.safeTransfer(msg.sender, amount);
+ _safeTransferWithAccounting(msg.sender, amount);
}
function claimSurvived() external nonReentrant {
// ...
- stakeToken.safeTransfer(msg.sender, payout);
+ _safeTransferWithAccounting(msg.sender, payout);
}
// ... apply to all outbound transfers