pragma solidity 0.8.20; import {Test} from "forge-std/Test.sol"; import {Fatso} from "../../src/FatsoERC20.sol"; import {Stinky} from "../../src/StinkyERC20.sol"; import {Ghost} from "../../src/GhstERC20.sol"; import {GhostAuthority} from "../../src/GhostAuthority.sol"; import {GhostStaking} from "../../src/Staking.sol"; import {ERC20PermitTest} from "./Permit.t.sol"; import {ERC20AllowanceTest} from "./Allowance.t.sol"; import {ERC20TransferTest} from "./Transfer.t.sol"; import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; contract StinkyTest is Test, ERC20PermitTest, ERC20AllowanceTest, ERC20TransferTest { using SafeERC20 for Stinky; uint256 private constant INITIAL_SHARES_SUPPLY = 5 * 10**15; // 15 or 16 uint256 private constant TOTAL_SHARES = type(uint256).max - (type(uint256).max % INITIAL_SHARES_SUPPLY); uint256 constant public INITIAL_INDEX = 10819917194513808e56; uint256 constant public TOTAL_INITIAL_SUPPLY = 5000000000000000; Fatso public ftso; Stinky public stnk; Ghost public ghst; GhostAuthority public authority; GhostStaking public staking; address constant INITIALIZER = 0x0000000000000000000000000000000000000001; address constant ALICE = 0x0000000000000000000000000000000000000003; address constant BOB = 0x0000000000000000000000000000000000000004; address constant TREASURY = 0x0000000000000000000000000000000000000005; uint256 constant AMOUNT = 69; string constant NAME = "Stinky Test Name"; string constant SYMBOL = "STNKTST"; event Transfer(address indexed from, address indexed to, uint256 value); event LogStakingContractUpdated(address stakingContract); function setUp() public { vm.startPrank(INITIALIZER); authority = new GhostAuthority( INITIALIZER, INITIALIZER, INITIALIZER, INITIALIZER ); ftso = new Fatso(address(authority), "Fatso", "FTSO"); stnk = new Stinky(INITIAL_INDEX, NAME, SYMBOL); ghst = new Ghost(address(stnk), "Ghost", "GHST"); staking = new GhostStaking( address(ftso), address(stnk), address(ghst), 69, 1337, 1337, address(authority) ); ghst.initialize(address(staking)); vm.stopPrank(); initializePermit(address(stnk), AMOUNT, TOTAL_INITIAL_SUPPLY); initializeAllowance(ALICE, BOB, address(stnk), AMOUNT, TOTAL_INITIAL_SUPPLY, TOTAL_INITIAL_SUPPLY); initializeTransfer(ALICE, BOB, address(stnk), AMOUNT, TOTAL_INITIAL_SUPPLY); } function test_isConstructedCorrectly() public view { assertEq(stnk.name(), NAME); assertEq(stnk.symbol(), SYMBOL); assertEq(stnk.decimals(), 9); assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY); assertEq(stnk.index(), INITIAL_INDEX / (TOTAL_SHARES / INITIAL_SHARES_SUPPLY)); } function test_initialization_couldBeDoneByInitializer() public { vm.prank(INITIALIZER); stnk.initialize(address(staking), TREASURY, address(ghst)); assertEq(stnk.staking(), address(staking)); assertEq(stnk.treasury(), TREASURY); assertEq(stnk.ghst(), address(ghst)); } function test_initialization_couldNotBeDoneByArbitraryAddress(address who) public { vm.assume(who != INITIALIZER); vm.expectRevert(); vm.prank(who); stnk.initialize(who, who, who); } function test_initialization_couldBeDoneOnlyOnce() public { vm.prank(INITIALIZER); stnk.initialize(address(staking), TREASURY, address(ghst)); vm.expectRevert(); vm.prank(INITIALIZER); stnk.initialize(INITIALIZER, INITIALIZER, INITIALIZER); } function test_initialization_correctTotalSharesOfStaking() public { _initialize(); assertEq(stnk.balanceOf(address(staking)), 5000000000000000); } function test_initialization_emitsTransferEvent() public { vm.expectEmit(true, true, true, true, address(stnk)); emit Transfer(address(0), address(staking), 5000000000000000); _initialize(); } function test_initialization_emitsLogStakingContractUpdatedEvent() public { vm.expectEmit(true, true, true, true, address(stnk)); emit LogStakingContractUpdated(address(staking)); _initialize(); } function test_circulatingSupplyIsCorrecBasedOnTotalSupply() public { _mintTokens(ALICE, AMOUNT); assertEq(stnk.circulatingSupply(), AMOUNT); assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY - AMOUNT); } function test_circulatingSupplyIsCorrecBasedOnStakingBalance() public { _mintTokens(ALICE, AMOUNT); assertEq(stnk.circulatingSupply(), AMOUNT); vm.prank(ALICE); stnk.safeTransfer(address(staking), AMOUNT); assertEq(stnk.circulatingSupply(), 0); assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY); } function test_circulatingSupplyIsCorrectBasedOnSupplyInWarmup() public { vm.startPrank(INITIALIZER); stnk.initialize(address(staking), TREASURY, address(ghst)); staking.setWarmupPeriod(1337); ftso.mint(ALICE, AMOUNT); vm.stopPrank(); assertEq(stnk.circulatingSupply(), 0); assertEq(ftso.totalSupply(), AMOUNT); assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY); assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY); vm.startPrank(ALICE); ftso.approve(address(staking), AMOUNT); staking.stake(AMOUNT, ALICE, true, true); vm.stopPrank(); assertEq(stnk.circulatingSupply(), AMOUNT); assertEq(ftso.totalSupply(), AMOUNT); assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY); assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY); } function test_circulatingSupplyIsCorrectBasedOnGhostSupply() public { vm.startPrank(INITIALIZER); stnk.initialize(address(staking), TREASURY, address(ghst)); ftso.mint(ALICE, AMOUNT); vm.stopPrank(); vm.startPrank(ALICE); ftso.approve(address(staking), AMOUNT); staking.stake(AMOUNT, ALICE, false, true); vm.stopPrank(); uint256 balanceTo = AMOUNT * 1e18 / ghst.index(); assertEq(ghst.balanceOf(ALICE), balanceTo); uint256 balanceFrom = ghst.totalSupply() * ghst.index() / 1e18; assertEq(stnk.circulatingSupply(), balanceFrom); } function test_toGhstCorrect(uint256 amountToTest) public { vm.assume(amountToTest < type(uint64).max); _initialize(); uint256 balanceTo = amountToTest * 1e18 / ghst.index(); assertEq(stnk.toGhst(amountToTest), ghst.balanceTo(amountToTest)); assertEq(stnk.toGhst(amountToTest), balanceTo); } function test_fromGhstCorrect(uint256 amountToTest) public { vm.assume(amountToTest < type(uint64).max); _initialize(); uint256 balanceFrom = amountToTest * ghst.index() / 1e18; assertEq(stnk.fromGhst(amountToTest), ghst.balanceFrom(amountToTest)); assertEq(stnk.fromGhst(amountToTest), balanceFrom); } function test_sharesForBalanceCorrect(uint256 amountToTest) public view { vm.assume(amountToTest < TOTAL_INITIAL_SUPPLY); uint256 sharesPerUnit = stnk.sharesForBalance(1); assertEq(stnk.sharesForBalance(1), sharesPerUnit); assertEq(stnk.sharesForBalance(amountToTest), amountToTest * sharesPerUnit); } function test_balance_forSharesCorrect(uint256 amountToTest) public view { vm.assume(amountToTest < type(uint128).max); uint256 sharesPerUnit = stnk.sharesForBalance(1); assertEq(stnk.balanceForShares(amountToTest), amountToTest / sharesPerUnit); } function test_debt_couldBeChangedByTreasury() public { _mintTokens(ALICE, AMOUNT); assertEq(stnk.debtBalances(ALICE), 0); vm.prank(TREASURY); stnk.changeDebt(AMOUNT, ALICE, true); assertEq(stnk.debtBalances(ALICE), AMOUNT); vm.prank(TREASURY); stnk.changeDebt(AMOUNT, ALICE, false); assertEq(stnk.debtBalances(ALICE), 0); } function test_debt_couldNotBeChangeByArbitraryAddress(address someone) public { vm.assume(someone != TREASURY); _mintTokens(ALICE, AMOUNT); assertEq(stnk.debtBalances(ALICE), 0); vm.expectRevert(); vm.prank(someone); stnk.changeDebt(AMOUNT, ALICE, true); } function test_balance_couldNotDropBelowDebt() public { _mintTokens(ALICE, AMOUNT * 3); vm.prank(TREASURY); stnk.changeDebt(AMOUNT, ALICE, true); assertEq(stnk.balanceOf(BOB), 0); vm.prank(ALICE); stnk.safeTransfer(BOB, AMOUNT); assertEq(stnk.balanceOf(BOB), AMOUNT); vm.expectRevert(); vm.prank(ALICE); assertEq(stnk.transfer(BOB, AMOUNT * 2), false); assertEq(stnk.balanceOf(BOB), AMOUNT); assertEq(stnk.balanceOf(ALICE), AMOUNT * 2); } function test_rebase_couldNotBeDoneFromArbitraryAddress(address someone) public { vm.assume(someone != address(staking)); vm.expectRevert(); vm.prank(someone); stnk.rebase(AMOUNT, 1337); } function test_rebase_nothingChangesIfDeltaIsZero() public { uint256 epoch = 1337; uint256 prevSupply = stnk.totalSupply(); uint256 prevIndex = stnk.index(); _mintTokens(ALICE, AMOUNT); assertEq(stnk.balanceOf(ALICE), AMOUNT); vm.prank(address(staking)); stnk.rebase(0, epoch); assertEq(stnk.totalSupply(), prevSupply); assertEq(stnk.index(), prevIndex); assertEq(stnk.balanceOf(ALICE), AMOUNT); } function test_rebase_changesIfProfitAndCirculatingSupply() public { uint256 epoch = 1337; _mintTokens(ALICE, AMOUNT); assertEq(stnk.balanceOf(ALICE), AMOUNT); vm.prank(address(staking)); stnk.rebase(AMOUNT, epoch); assertEq(stnk.balanceOf(ALICE), AMOUNT * 2); assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY * 2); } function test_rebase_changesIfProfitAndNoCirculatingSupply() public { uint256 epoch = 1337; _initialize(); assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY); vm.prank(address(staking)); stnk.rebase(AMOUNT, epoch); assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY + AMOUNT); assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY + AMOUNT); } function test_rebase_couldNotExceedMaxSupply() public { uint256 epoch = 1337; _initialize(); assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY); vm.prank(address(staking)); stnk.rebase(type(uint128).max, epoch); assertEq(stnk.balanceOf(address(staking)), type(uint128).max); assertEq(stnk.totalSupply(), type(uint128).max); } function _initialize() internal { vm.prank(INITIALIZER); stnk.initialize(address(staking), TREASURY, address(ghst)); assertEq(stnk.staking(), address(staking)); assertEq(stnk.treasury(), TREASURY); assertEq(stnk.ghst(), address(ghst)); } function _mintTokens(address who, uint256 value) internal { _initialize(); uint256 totalSupply = stnk.totalSupply(); vm.prank(address(staking)); stnk.safeTransfer(who, value); assertEq(stnk.totalSupply(), totalSupply); } function _mintTransferTokens(address who, uint256 value) internal override { _mintTokens(who, value); } function _mintAllowanceTokens(address who, uint256 value) internal override { _mintTokens(who, value); } function _mintPermitTokens(address who, uint256 value) internal override { _mintTokens(who, value); } function _getCurrentTotalSupply() internal override pure returns (uint256) { return TOTAL_INITIAL_SUPPLY; } }