# Review package: 2a21766237a9c29970519f7fa00482faecd06fdd..6dcbb03f3c79979416c2f0dd37556539276a4e29 ## Commits 6dcbb03 test: prove V1 accounting invariants ## Files changed test/BankInvariant.t.sol | 43 ++++++++++++++++++++++++ test/helpers/BankHandler.sol | 79 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) ## Diff diff --git a/test/BankInvariant.t.sol b/test/BankInvariant.t.sol new file mode 100644 index 0000000..f699902 --- /dev/null +++ b/test/BankInvariant.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.35; + +import {BankTestBase} from "./helpers/BankTestBase.sol"; +import {BankHandler} from "./helpers/BankHandler.sol"; + +contract BankInvariantTest is BankTestBase { + BankHandler internal handler; + + function setUp() public override { + super.setUp(); + + handler = new BankHandler(token, bank); + vm.prank(owner); + token.transferOwnership(address(handler)); + + targetContract(address(handler)); + bytes4[] memory selectors = new bytes4[](3); + selectors[0] = handler.deposit.selector; + selectors[1] = handler.withdraw.selector; + selectors[2] = handler.donate.selector; + targetSelector(FuzzSelector({addr: address(handler), selectors: selectors})); + } + + function invariant_liabilitiesEqualTrackedBalances() public view { + uint256 sum; + for (uint256 i; i < handler.actorCount(); ++i) { + sum += bank.balanceOf(handler.actorAt(i)); + } + assertEq(sum, bank.totalLiabilities()); + } + + function invariant_reservesCoverLiabilities() public view { + assertGe(token.balanceOf(address(bank)), bank.totalLiabilities()); + } + + function invariant_ghostAccountingMatchesChain() public view { + assertEq(handler.ghostDeposited() - handler.ghostWithdrawn(), bank.totalLiabilities()); + assertEq( + handler.ghostDeposited() + handler.ghostDonated() - handler.ghostWithdrawn(), token.balanceOf(address(bank)) + ); + } +} diff --git a/test/helpers/BankHandler.sol b/test/helpers/BankHandler.sol new file mode 100644 index 0000000..20ec183 --- /dev/null +++ b/test/helpers/BankHandler.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.35; + +import {Test} from "forge-std/Test.sol"; + +import {BankV1} from "../../src/BankV1.sol"; +import {MockUSDC} from "../../src/MockUSDC.sol"; + +contract BankHandler is Test { + MockUSDC internal immutable token; + BankV1 internal immutable bank; + + address internal immutable actor0; + address internal immutable actor1; + address internal immutable actor2; + address internal immutable actor3; + + uint256 public ghostDeposited; + uint256 public ghostWithdrawn; + uint256 public ghostDonated; + + constructor(MockUSDC token_, BankV1 bank_) { + token = token_; + bank = bank_; + actor0 = address(0x1001); + actor1 = address(0x1002); + actor2 = address(0x1003); + actor3 = address(0x1004); + } + + function deposit(uint256 actorSeed, uint256 amount) external { + address actor = actorAt(actorSeed % actorCount()); + amount = bound(amount, 1, 10_000e6); + token.mint(actor, amount); + + vm.startPrank(actor); + token.approve(address(bank), amount); + bank.deposit(amount); + vm.stopPrank(); + + ghostDeposited += amount; + } + + function withdraw(uint256 actorSeed, uint256 amount) external { + address actor = actorAt(actorSeed % actorCount()); + uint256 balance = bank.balanceOf(actor); + if (balance == 0) return; + + amount = bound(amount, 1, balance); + vm.startPrank(actor); + bank.withdraw(amount); + vm.stopPrank(); + + ghostWithdrawn += amount; + } + + function donate(uint256 actorSeed, uint256 amount) external { + address actor = actorAt(actorSeed % actorCount()); + amount = bound(amount, 1, 1_000e6); + token.mint(actor, amount); + + vm.startPrank(actor); + token.transfer(address(bank), amount); + vm.stopPrank(); + + ghostDonated += amount; + } + + function actorCount() public pure returns (uint256) { + return 4; + } + + function actorAt(uint256 index) public view returns (address) { + if (index == 0) return actor0; + if (index == 1) return actor1; + if (index == 2) return actor2; + return actor3; + } +}