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 {GhostTreasury} from "../../src/Treasury.sol"; import {GhostStaking} from "../../src/Staking.sol"; import {GhostBondDepository} from "../../src/BondDepository.sol"; import {ERC20Mock} from "../../src/mocks/ERC20Mock.sol"; import {GhostBondingCalculator} from "../../src/StandardBondingCalculator.sol"; import {ITreasury} from "../../src/interfaces/ITreasury.sol"; contract GhostBondDepositoryTest is Test { uint256 public constant TOTAL_INITIAL_SUPPLY = 5000000000000000; uint256 public constant LARGE_APPROVAL = 100000000000000000000000000000000; uint256 public constant INITIAL_INDEX = 10819917194513808e56; uint48 public constant EPOCH_LENGTH = 2200; uint48 public constant EPOCH_NUMBER = 1; uint48 public constant EPOCH_END_TIME = 1337; uint256 public constant INITIAL_MINT = 1000000000000000000000000; uint256 public constant CAPACITY = 10000e9; uint256 public constant INITIAL_PRICE = 400e9; uint256 public constant BUFFER = 2e5; address constant INITIALIZER = 0x0000000000000000000000000000000000000001; address constant GOVERNOR = 0x0000000000000000000000000000000000000003; address constant GUARDIAN = 0x0000000000000000000000000000000000000004; address constant POLICY = 0x0000000000000000000000000000000000000005; address constant VAULT = 0x0000000000000000000000000000000000000006; address constant ALICE = 0x0000000000000000000000000000000000000007; uint256 public constant VESTING = 100; uint256 public constant TIME_TO_CONCLUSION = 60 * 60 * 24; uint256 public constant DEPOSIT_INTERVAL = 60 * 60 * 4; uint256 public constant TUNE_INTERVAL = 60 * 60; uint256 public conclusion; ERC20Mock reserve; Fatso ftso; Stinky stnk; Ghost ghst; GhostStaking staking; GhostTreasury treasury; GhostAuthority authority; GhostBondDepository depository; GhostBondingCalculator calculator; function setUp() public { vm.startPrank(INITIALIZER); authority = new GhostAuthority( GOVERNOR, GUARDIAN, POLICY, VAULT ); reserve = new ERC20Mock("Reserve Token", "RET"); ftso = new Fatso(address(authority), "Fatso", "FTSO"); stnk = new Stinky(INITIAL_INDEX, "Stinky", "STNK"); ghst = new Ghost(address(stnk), "Ghost", "GHST"); staking = new GhostStaking( address(ftso), address(stnk), address(ghst), EPOCH_LENGTH, EPOCH_NUMBER, EPOCH_END_TIME, address(authority) ); treasury = new GhostTreasury(address(ftso), 69, address(authority)); calculator = new GhostBondingCalculator(address(ftso), 1, 1); stnk.initialize(address(staking), address(treasury), address(ghst)); ghst.initialize(address(staking)); depository = new GhostBondDepository( address(authority), address(ftso), address(ghst), address(staking), address(treasury) ); vm.stopPrank(); _createFirstBond(); } function _createFirstBond() internal { vm.startPrank(GOVERNOR); authority.pushVault(address(treasury)); treasury.enable(ITreasury.STATUS.REWARDMANAGER, address(depository), address(0)); treasury.enable(ITreasury.STATUS.RESERVEDEPOSITOR, ALICE, address(0)); treasury.enable(ITreasury.STATUS.RESERVETOKEN, address(reserve), address(calculator)); vm.stopPrank(); vm.startPrank(ALICE); reserve.mint(ALICE, INITIAL_MINT); reserve.approve(address(treasury), type(uint256).max); treasury.deposit(address(reserve), INITIAL_MINT, treasury.tokenValue(address(reserve), INITIAL_MINT) / 2); assertEq(ftso.totalSupply(), treasury.baseSupply()); reserve.mint(ALICE, INITIAL_MINT); reserve.approve(address(depository), type(uint256).max); vm.stopPrank(); conclusion = block.timestamp + TIME_TO_CONCLUSION; vm.prank(POLICY); depository.create( [CAPACITY, INITIAL_PRICE, BUFFER], [VESTING, conclusion], address(reserve), [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast) [false, true] ); } function test_shouldCreateMarket() public view { assertEq(depository.isLive(0), true); } function test_shouldConcludeInCorrectAmountOfTime() public view { (, , , uint48 concludes,) = depository.terms(0); assertEq(concludes, uint48(conclusion)); // forge-lint: disable-line(unsafe-typecast) (, , uint48 length, , ,) = depository.metadatas(0); assertEq(length, TIME_TO_CONCLUSION); } function test_shouldSetMaxPayoutToCorrectPercentageOfCapacity() public view { (, , , , uint256 maxPayout, ,) = depository.markets(0); assertEq(maxPayout, CAPACITY / 6); } function test_shouldReturnIdsOfAllMarkets() public { vm.prank(POLICY); depository.create( [CAPACITY, INITIAL_PRICE, BUFFER], [VESTING, conclusion], address(reserve), [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast) [false, true] ); uint256[] memory liveMarkets = depository.liveMarkets(); assertEq(liveMarkets.length, 2); assertEq(liveMarkets[0], 0); assertEq(liveMarkets[1], 1); } function test_shouldUpdateIdsOfMarkets() public { vm.startPrank(POLICY); depository.create( [CAPACITY, INITIAL_PRICE, BUFFER], [VESTING, conclusion], address(reserve), [uint32(DEPOSIT_INTERVAL), uint32(TUNE_INTERVAL)], // forge-lint: disable-line(unsafe-typecast) [false, true] ); depository.close(0); vm.stopPrank(); uint256[] memory liveMarkets = depository.liveMarkets(); assertEq(liveMarkets.length, 1); assertEq(liveMarkets[0], 1); } function test_shouldIncludeIdInLiveMarketsForQuotePeople() public view { uint256[] memory liveMarketsFor = depository.liveMarketsFor(address(reserve)); assertEq(liveMarketsFor.length, 1); assertEq(liveMarketsFor[0], 0); } function test_shouldStartWithPriceAtInitialPrice() public view { assertEq(depository.marketPrice(0), INITIAL_PRICE); } function test_shouldGiveAccuratePayoutForPrice() public view { uint256 price = depository.marketPrice(0); uint256 amount = 10_000 * 1e18; uint256 expectedPrice = amount / price; assertEq(depository.payoutFor(0, amount), expectedPrice); } function test_shouldDecayDebt() public { (, , , uint256 totalDebt, , ,) = depository.markets(0); skip(DEPOSIT_INTERVAL); vm.prank(ALICE); depository.deposit(0, 0, INITIAL_PRICE, ALICE, ALICE); (, , , uint256 newTotalDebt, , ,) = depository.markets(0); assertEq(totalDebt > newTotalDebt, true); } function test_shouldStartAdjustmentIfBehindSchedule() public { skip(DEPOSIT_INTERVAL); uint256 amount = 10_000 * 1e18; vm.prank(ALICE); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); (, , , bool active) = depository.adjustments(0); assertEq(active, true); } function test_adjustmentShouldLowerControlVariableByChangeInTuneIntervalIfBehind() public { (, uint64 ctrlVariable, , ,) = depository.terms(0); uint256 amount = 10_000 * 1e18; vm.startPrank(ALICE); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); skip(DEPOSIT_INTERVAL); (uint64 change, , ,) = depository.adjustments(0); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); vm.stopPrank(); (, uint64 newCtrlVariable, , ,) = depository.terms(0); assertEq(newCtrlVariable, ctrlVariable - change); } function test_adjustmentShouldLowerControlVariableByHalfOfTuneInterval() public { skip(DEPOSIT_INTERVAL); (, uint64 ctrlVariable, , ,) = depository.terms(0); uint256 amount = 10_000 * 1e18; vm.startPrank(ALICE); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); (uint64 change, , ,) = depository.adjustments(0); skip(TUNE_INTERVAL / 2); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); (, uint64 newCtrlVariable, , ,) = depository.terms(0); vm.stopPrank(); uint256 lowerBound = (ctrlVariable - change / 2) * 9999 / 10000; assertEq(newCtrlVariable <= ctrlVariable - (change / 2), true); assertEq(newCtrlVariable > lowerBound, true); } function test_adjustmentShouldContinueLoweringOverMultipleDepositsInSameInterval() public { (, uint64 ctrlVariable, , ,) = depository.terms(0); uint256 amount = 10_000 * 1e18; vm.startPrank(ALICE); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); (uint64 change, , ,) = depository.adjustments(0); skip(TUNE_INTERVAL / 2); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); skip(TUNE_INTERVAL / 2); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); vm.stopPrank(); (, uint64 newCtrlVariable, , ,) = depository.terms(0); assertEq(newCtrlVariable, ctrlVariable - change); } function test_shouldAllowDeposit() public { uint256 amount = 10_000 * 1e18; vm.prank(ALICE); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); uint256[] memory arr = depository.indexesFor(ALICE); assertEq(arr.length, 1); } function test_shouldNotAllowDepositGreaterThanMaxPayout() public { uint256 amount = 6_700_000 * 1e18; vm.expectRevert(); vm.prank(ALICE); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); } function test_shouldNotRedeemAfterVested() public { uint256 balance = ftso.balanceOf(ALICE); uint256 amount = 10_000 * 1e18; // 10,000 vm.startPrank(ALICE); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); depository.redeemAll(ALICE, true); vm.stopPrank(); assertEq(ftso.balanceOf(ALICE), balance); } function test_shouldRedeemAfterVested() public { uint256 amount = 10_000 * 1e18; // 10,000 vm.startPrank(ALICE); (uint256 expectedPayout, ,) = depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); skip(DEPOSIT_INTERVAL); depository.redeemAll(ALICE, true); vm.stopPrank(); uint256 aliceBalance = ghst.balanceOf(ALICE); assertEq(aliceBalance >= ghst.balanceTo(expectedPayout), true); assertEq(aliceBalance < ghst.balanceTo(expectedPayout * 10001 / 10000), true); } function test_shouldCorrectlyRedeemPartially() public { uint256 amount = 1 * 1e18; vm.startPrank(ALICE); depository.deposit(0, amount, INITIAL_PRICE * 2, ALICE, ALICE); depository.deposit(0, amount, INITIAL_PRICE * 2, ALICE, ALICE); depository.deposit(0, amount, INITIAL_PRICE * 2, ALICE, ALICE); depository.deposit(0, amount, INITIAL_PRICE * 2, ALICE, ALICE); skip(DEPOSIT_INTERVAL); uint256[] memory indexesToRemove = new uint256[](1); indexesToRemove[0] = 1; uint256[] memory nextIndexToRemove = new uint256[](1); nextIndexToRemove[0] = 3; uint256[] memory allIndexes = depository.indexesFor(ALICE); assertEq(allIndexes.length, 4); assertEq(allIndexes[0], 0); assertEq(allIndexes[1], 1); assertEq(allIndexes[2], 2); assertEq(allIndexes[3], 3); depository.redeem(ALICE, true, indexesToRemove); uint256[] memory allIndexesOneRemoved = depository.indexesFor(ALICE); assertEq(allIndexesOneRemoved.length, 3); assertEq(allIndexesOneRemoved[0], 0); assertEq(allIndexesOneRemoved[1], 3); assertEq(allIndexesOneRemoved[2], 2); depository.redeem(ALICE, true, nextIndexToRemove); uint256[] memory allIndexesTwoRemoved = depository.indexesFor(ALICE); assertEq(allIndexesTwoRemoved.length, 2); assertEq(allIndexesTwoRemoved[0], 0); assertEq(allIndexesTwoRemoved[1], 2); vm.stopPrank(); } function test_afterSuccesfullWarmupAutoClaimExecuted() public { uint256 amount = 10_000 * 1e18; // 10,000 vm.prank(GOVERNOR); staking.setWarmupPeriod(1); vm.startPrank(ALICE); assertEq(ghst.balanceOf(ALICE), 0); (uint256 expectedPayout, ,) = depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); assertEq(ghst.balanceOf(address(depository)), 0); skip(DEPOSIT_INTERVAL); staking.rebase(); depository.redeemAll(ALICE, true); uint256 aliceBalance = ghst.balanceOf(ALICE); assertEq(aliceBalance >= ghst.balanceTo(expectedPayout), true); assertEq(aliceBalance < ghst.balanceTo(expectedPayout * 10001 / 10000), true); vm.stopPrank(); } function test_externalAccountCouldNotClaimFromWarmup() public { vm.startPrank(ALICE); vm.expectRevert(); staking.claim(address(depository), false); vm.expectRevert(); staking.claim(address(depository), true); vm.stopPrank(); } function test_shouldDecayMaxPayoutInTargetDepositInterval() public { (, , , , uint64 maxPayout, ,) = depository.markets(0); uint256 price = depository.marketPrice(0); uint256 amount = maxPayout * price; vm.prank(ALICE); depository.deposit(0, amount, INITIAL_PRICE, ALICE, ALICE); skip(DEPOSIT_INTERVAL); uint256 newPrice = depository.marketPrice(0); assertEq(newPrice < INITIAL_PRICE, true); } function test_shouldCloseMarket() public { (uint256 cap, , , , , ,) = depository.markets(0); assertEq(cap > 0, true); vm.prank(POLICY); depository.close(0); (uint256 newCap, , , , , ,) = depository.markets(0); assertEq(newCap, 0); } }