pragma solidity 0.8.20; import {Test} from "forge-std/Test.sol"; import "../../src/FatsoERC20.sol"; import "../../src/StinkyERC20.sol"; import "../../src/GhstERC20.sol"; import "../../src/GhostAuthority.sol"; import "../../src/StakingDistributor.sol"; import "../../src/Treasury.sol"; import "../../src/Staking.sol"; import "../../src/BondDepository.sol"; import "../../src/mocks/ERC20Mock.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 initialMint = 10000000000000000000000000; uint256 public constant initialDeposit = 1000000000000000000000000; uint256 public constant capacity = 10000e9; uint256 public constant initialPrice = 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; address constant bob = 0x0000000000000000000000000000000000000008; uint256 public constant vesting = 100; uint256 public constant timeToConclusion = 60 * 60 * 24; uint256 public constant depositInterval = 60 * 60 * 4; uint256 public constant tuneInterval = 60 * 60; uint256 public conclusion; ERC20Mock reserve; Fatso ftso; Stinky stnk; Ghost ghst; GhostStaking staking; GhostTreasury treasury; GhostAuthority authority; GhostBondDepository depository; function setUp() public { vm.startPrank(initializer); authority = new GhostAuthority( governor, guardian, policy, vault ); reserve = new ERC20Mock("Reserve Token", "RET"); ftso = new Fatso(address(authority)); stnk = new Stinky(INITIAL_INDEX); ghst = new Ghost(address(stnk)); 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)); 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(0)); vm.stopPrank(); vm.startPrank(alice); reserve.mint(alice, initialMint); reserve.approve(address(treasury), type(uint256).max); treasury.deposit(address(reserve), initialMint, treasury.tokenValue(address(reserve), initialMint) / 2); assertEq(ftso.totalSupply(), treasury.baseSupply()); reserve.mint(alice, initialMint); reserve.approve(address(depository), type(uint256).max); vm.stopPrank(); conclusion = block.timestamp + timeToConclusion; vm.prank(policy); depository.create( [capacity, initialPrice, buffer], [vesting, conclusion], address(reserve), [uint32(depositInterval), uint32(tuneInterval)], [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)); (, , uint48 length, , ,) = depository.metadatas(0); assertEq(length, timeToConclusion); } function test_shouldSetMaxPayoutToCorrectPercentageOfCapacity() public view { (, , , , uint256 maxPayout, ,) = depository.markets(0); assertEq(maxPayout, capacity / 6); } function test_shouldReturnIdsOfAllMarkets() public { vm.prank(policy); depository.create( [capacity, initialPrice, buffer], [vesting, conclusion], address(reserve), [uint32(depositInterval), uint32(tuneInterval)], [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, initialPrice, buffer], [vesting, conclusion], address(reserve), [uint32(depositInterval), uint32(tuneInterval)], [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), initialPrice); } 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(depositInterval); vm.prank(alice); depository.deposit(0, 0, initialPrice, alice, alice); (, , , uint256 newTotalDebt, , ,) = depository.markets(0); assertEq(totalDebt > newTotalDebt, true); } function test_shouldStartAdjustmentIfBehindSchedule() public { skip(depositInterval); uint256 amount = 10_000 * 1e18; vm.prank(alice); depository.deposit(0, amount, initialPrice, 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, initialPrice, alice, alice); skip(depositInterval); (uint64 change, , ,) = depository.adjustments(0); depository.deposit(0, amount, initialPrice, alice, alice); vm.stopPrank(); (, uint64 newCtrlVariable, , ,) = depository.terms(0); assertEq(newCtrlVariable, ctrlVariable - change); } function test_adjustmentShouldLowerControlVariableByHalfOfTuneInterval() public { skip(depositInterval); (, uint64 ctrlVariable, , ,) = depository.terms(0); uint256 amount = 10_000 * 1e18; vm.startPrank(alice); depository.deposit(0, amount, initialPrice, alice, alice); (uint64 change, , ,) = depository.adjustments(0); skip(tuneInterval / 2); depository.deposit(0, amount, initialPrice, 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, initialPrice, alice, alice); (uint64 change, , ,) = depository.adjustments(0); skip(tuneInterval / 2); depository.deposit(0, amount, initialPrice, alice, alice); skip(tuneInterval / 2); depository.deposit(0, amount, initialPrice, 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, initialPrice, 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, initialPrice, 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, initialPrice, 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, initialPrice, alice, alice); skip(depositInterval); 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, initialPrice * 2, alice, alice); depository.deposit(0, amount, initialPrice * 2, alice, alice); depository.deposit(0, amount, initialPrice * 2, alice, alice); depository.deposit(0, amount, initialPrice * 2, alice, alice); skip(depositInterval); 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, initialPrice, alice, alice); assertEq(ghst.balanceOf(address(depository)), 0); skip(depositInterval); 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, initialPrice, alice, alice); skip(depositInterval); uint256 newPrice = depository.marketPrice(0); assertEq(newPrice < initialPrice, 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); } }