The precision loss is demonstrated through comprehensive tests showing that tokens are permanently locked in the contract due to integer division truncation.
pragma solidity 0.8.26;
import {Test, console} from "forge-std/Test.sol";
import {InheritanceManager} from "../src/InheritanceManager.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
contract PrecisionLossTest is Test {
InheritanceManager im;
ERC20Mock token;
address owner = makeAddr("owner");
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
function setUp() public {
vm.prank(owner);
im = new InheritanceManager();
token = new ERC20Mock();
}
function test_precisionLossWithdrawERC20() public {
vm.startPrank(owner);
im.addBeneficiery(user1);
im.addBeneficiery(user2);
im.addBeneficiery(user3);
vm.stopPrank();
token.mint(address(im), 10);
vm.warp(90 days + 1);
vm.prank(user1);
im.inherit();
uint256 preContractBalance = token.balanceOf(address(im));
uint256 preUser1Balance = token.balanceOf(user1);
uint256 preUser2Balance = token.balanceOf(user2);
uint256 preUser3Balance = token.balanceOf(user3);
vm.prank(user1);
im.withdrawInheritedFunds(address(token));
uint256 postContractBalance = token.balanceOf(address(im));
uint256 postUser1Balance = token.balanceOf(user1);
uint256 postUser2Balance = token.balanceOf(user2);
uint256 postUser3Balance = token.balanceOf(user3);
uint256 distributedAmount = (postUser1Balance - preUser1Balance) +
(postUser2Balance - preUser2Balance) +
(postUser3Balance - preUser3Balance);
uint256 dustAmount = preContractBalance - distributedAmount;
console.log("Pre-contract balance:", preContractBalance);
console.log("Distributed amount:", distributedAmount);
console.log("Dust amount trapped:", dustAmount);
console.log("Post-contract balance:", postContractBalance);
assertEq(postUser1Balance - preUser1Balance, 3);
assertEq(postUser2Balance - preUser2Balance, 3);
assertEq(postUser3Balance - preUser3Balance, 3);
assertEq(postContractBalance, 1);
assertEq(dustAmount, 1);
}
function test_precisionLossBuyOutEstateNFT() public {
vm.startPrank(owner);
im.addBeneficiery(user1);
im.addBeneficiery(user2);
im.addBeneficiery(user3);
im.createEstateNFT("Test Property", 10e6, address(token));
vm.stopPrank();
token.mint(user1, 10e6);
vm.warp(90 days + 1);
vm.prank(user1);
im.inherit();
uint256 preUser2Balance = token.balanceOf(user2);
uint256 preUser3Balance = token.balanceOf(user3);
vm.startPrank(user1);
token.approve(address(im), 10e6);
im.buyOutEstateNFT(1);
vm.stopPrank();
uint256 postUser2Balance = token.balanceOf(user2);
uint256 postUser3Balance = token.balanceOf(user3);
uint256 value = 10e6;
uint256 divisor = 3;
uint256 multiplier = 2;
uint256 finalAmount = (value / divisor) * multiplier;
uint256 expectedPerUser = finalAmount / divisor;
uint256 firstDivisionDust = value - (value / divisor * divisor);
uint256 secondDivisionDust = finalAmount - (expectedPerUser * divisor);
uint256 totalDust = firstDivisionDust + secondDivisionDust;
console.log("NFT Value:", value);
console.log("First division result (value/divisor):", value/divisor);
console.log("finalAmount calculated:", finalAmount);
console.log("Expected per user:", expectedPerUser);
console.log("First division dust:", firstDivisionDust);
console.log("Second division dust:", secondDivisionDust);
console.log("Total dust:", totalDust);
console.log("User2 balance diff:", postUser2Balance - preUser2Balance);
console.log("User3 balance diff:", postUser3Balance - preUser3Balance);
assertEq(firstDivisionDust, 1, "First division should result in 1 token dust");
}
}
The vulnerability affects both ERC20 tokens and native ETH, though the impact is relatively small per transaction. With multiple beneficiaries and frequent transactions, the accumulated dust could become more significant over time.
function withdrawInheritedFunds(address _asset) external {
if (!isInherited) {
revert NotYetInherited();
}
uint256 beneficiaryCount = beneficiaries.length;
if (_asset == address(0)) {
uint256 ethAmountAvailable = address(this).balance;
for (uint256 i = 0; i < beneficiaryCount - 1; i++) {
address payable beneficiary = payable(beneficiaries[i]);
uint256 amountPerBeneficiary = ethAmountAvailable / beneficiaryCount;
(bool success,) = beneficiary.call{value: amountPerBeneficiary}("");
require(success, "something went wrong");
ethAmountAvailable -= amountPerBeneficiary;
}
address payable lastBeneficiary = payable(beneficiaries[beneficiaryCount - 1]);
(bool success,) = lastBeneficiary.call{value: ethAmountAvailable}("");
require(success, "something went wrong");
} else {
}
}