diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..ba2591d --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,32 @@ +on: [ push, pull_request, workflow_dispatch ] + +name: integration-test + +jobs: + integration-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [ 18 ] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: yarn + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - id: forge_test + run: forge test -vvv --mc BaseIntegrationTest --fork-url https://mainnet.optimism.io + - name: income simulator + run: mkdir reports && forge test -vv --mp "test/integration/IncomeSimulator.t.sol" --fork-url https://mainnet.optimism.io + continue-on-error: true + - name: Archive Simulator Report + uses: actions/upload-artifact@v4 + with: + name: income-simulator-report + path: | + reports/*.txt + diff --git a/.github/workflows/invariant-test.yml b/.github/workflows/invariant-test.yml new file mode 100644 index 0000000..862b755 --- /dev/null +++ b/.github/workflows/invariant-test.yml @@ -0,0 +1,24 @@ +on: [ workflow_dispatch ] + +name: invariant-test + +jobs: + invariant-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [ 18 ] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: yarn + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - id: forge_test + run: forge test -vvv --mc BaseInvariantTest --fork-url https://optimism.llamarpc.com/sk_llama_115e7405eff4c29287d6ff9a0275bf84 + continue-on-error: true + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14eeb58..938f784 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,12 +19,12 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - name: Cache fork requests - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.foundry/cache key: "${{ runner.os }}-foundry-network-fork-${{ github.sha }}" restore-keys: | ${{ runner.os }}-foundry-network-fork- - id: forge_test - run: forge test --fork-url https://mainnet.optimism.io + run: forge test -vvv --mp "test/unit/*.sol" --fork-url https://mainnet.optimism.io continue-on-error: true diff --git a/.gitignore b/.gitignore index d86023a..f700538 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,7 @@ node_modules .vscode .DS_Store coverage.json -lcov.info \ No newline at end of file +lcov.info + +report.txt +reports \ No newline at end of file diff --git a/contracts/lib/BondingCurveLib.sol b/contracts/lib/BondingCurveLib.sol index 25b6127..66bd5e0 100644 --- a/contracts/lib/BondingCurveLib.sol +++ b/contracts/lib/BondingCurveLib.sol @@ -24,7 +24,9 @@ library BondingCurveLib { uint256 h = inflectionPrice; // Early return to save gas if either `g` or `h` is zero. - if (g * h == 0) return 0; + if (g * h == 0) { + return 0; + } uint256 s = uint256(fromSupply) + 1; uint256 end = s + uint256(quantity); diff --git a/foundry.toml b/foundry.toml index 8c410e1..5a78660 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,6 +8,7 @@ out = 'out' libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache-foundry' +fs_permissions = [{ access = "write", path = "./"}] [fmt] line_length = 120 # Maximum line length where formatter will try to wrap the line @@ -19,4 +20,14 @@ quote_style = 'double' [fuzz] runs = 10 -# See more config options https://github.com/gakonst/foundry/tree/master/config +[invariant] +runs = 100 +depth = 1 +verbosity = 4 +fail_on_revert = false +call_override = false +dictionary_weight = 80 +include_storage = true +include_push_bytes = true + +# See more config options https://github.com/foundry-rs/foundry/tree/master/crates/config diff --git a/package.json b/package.json index 36a74f6..908d368 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,11 @@ "scripts": { "build": "forge build", "compile": "forge compile", - "test": "forge test --fork-url https://mainnet.optimism.io", - "coverage": "forge coverage --fork-url https://mainnet.optimism.io", + "test": "forge test -vvv --mp \"test/unit/**/*.sol\" --fork-url https://mainnet.optimism.io", + "test:integration": "forge test -vvv --mp \"test/integration/*/*.sol\" --fork-url https://mainnet.optimism.io ", + "test:invariant": "forge test -vvv --mc BaseInvariantTest --fork-url https://optimism.llamarpc.com/sk_llama_3f92d666a172604faf69e469a67ec6ea ", + "test:income": "forge test -vvvv --mp \"test/integration/IncomeSimulator.t.sol\" --fork-url https://mainnet.optimism.io ", + "coverage": "forge coverage --mp \"test/unit/**/*.sol\" --fork-url https://mainnet.optimism.io ", "deploy:testnet": "make deploy-testnet", "deploy:mainnet": "make deploy-mainnet", "fmt": "forge fmt --check && solhint \"{scripts,contracts,test}/**/*.sol\"", diff --git a/remappings.txt b/remappings.txt index 4aee4be..d0fd4ba 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,4 @@ contracts/=contracts/ @openzeppelin/contracts=node_modules/@openzeppelin/contracts solady/=node_modules/solady/src/ forge-std/=node_modules/forge-std/src/ -ds-test/=node_modules/ds-test/src \ No newline at end of file +ds-test/src/=node_modules/ds-test/src/ diff --git a/test/integration/BaseIntegrationTest.t.sol b/test/integration/BaseIntegrationTest.t.sol new file mode 100644 index 0000000..e44202b --- /dev/null +++ b/test/integration/BaseIntegrationTest.t.sol @@ -0,0 +1,175 @@ +pragma solidity ^0.8.25; + +import { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SharesFactoryV1 } from "contracts/core/SharesFactoryV1.sol"; +import { SharesERC1155 } from "contracts/core/SharesERC1155.sol"; +import { AaveYieldAggregator } from "contracts/core/aggregator/AaveYieldAggregator.sol"; +import { BlankYieldAggregator } from "contracts/core/aggregator/BlankYieldAggregator.sol"; +import { IAavePool } from "contracts/interface/IAave.sol"; + +contract BaseIntegrationTest is Test { + address public SHARES_FOUNDER = makeAddr("founder"); + address public FACTORY_OWNER = makeAddr("factoryOwner"); + + // spam roles + address public SPAM_BOT = makeAddr("spamBot"); + address public HACKER = makeAddr("hacker"); + + // normal traders + address public DAILY_TRADER = makeAddr("dailyTrader"); + address public LONG_TERM_TRADER = makeAddr("longTermTrader"); + + struct PresetCurveParams { + uint96 basePrice; + uint32 inflectionPoint; + uint128 inflectionPrice; + uint128 linearPriceSlope; + } + + SharesERC1155 public sharesNFT; + SharesFactoryV1 public sharesFactory; + + AaveYieldAggregator public aaveYieldAggregator; + BlankYieldAggregator public blankYieldAggregator; + + address public constant WETH = 0x4200000000000000000000000000000000000006; + address public constant AAVE_POOL = 0x794a61358D6845594F94dc1DB02A252b5b4814aD; + address public constant AAVE_WETH_GATEWAY = 0xe9E52021f4e11DEAD8661812A0A6c8627abA2a54; + + IERC20 public aWETH = IERC20(IAavePool(AAVE_POOL).getReserveData(WETH).aTokenAddress); + + string public constant BASE_URI = "https://foo.com/shares/uri/"; + + PresetCurveParams public DEFAULT_CURVE_PARAMS = PresetCurveParams({ + basePrice: 0.005 ether, + inflectionPoint: 1500, + inflectionPrice: 0.01025 ether, + linearPriceSlope: 0 + }); + + PresetCurveParams public STANDARD_CURVE_PARAMS = PresetCurveParams({ + basePrice: 0.001 ether, + inflectionPoint: 1500, + inflectionPrice: 0.002 ether, + linearPriceSlope: 0 + }); + + PresetCurveParams public EXCLUSIVE_CURVE_PARAMS = PresetCurveParams({ + basePrice: 0.01 ether, + inflectionPoint: 1500, + inflectionPrice: 0.02 ether, + linearPriceSlope: 0 + }); + + uint8 public constant DEFAULT_CURVE_TYPE = 0; + uint8 public constant STANDARD_CURVE_TYPE = 1; + uint8 public constant EXCLUSIVE_CURVE_TYPE = 2; + + // common deploy contracts with args, call if needed + function deployContracts() public { + vm.startPrank(FACTORY_OWNER); + sharesNFT = new SharesERC1155(BASE_URI); + + sharesFactory = new SharesFactoryV1( + address(sharesNFT), + DEFAULT_CURVE_PARAMS.basePrice, + DEFAULT_CURVE_PARAMS.inflectionPoint, + DEFAULT_CURVE_PARAMS.inflectionPrice, + DEFAULT_CURVE_PARAMS.linearPriceSlope + ); + + aaveYieldAggregator = new AaveYieldAggregator(address(sharesFactory), WETH, AAVE_POOL, AAVE_WETH_GATEWAY); + blankYieldAggregator = new BlankYieldAggregator(address(sharesFactory), WETH); + + + sharesNFT.setFactory(address(sharesFactory)); + sharesFactory.resetYield(address(blankYieldAggregator)); + + sharesNFT.transferOwnership(FACTORY_OWNER); + aaveYieldAggregator.transferOwnership(FACTORY_OWNER); + + sharesFactory.transferOwnership(FACTORY_OWNER); + sharesFactory.acceptOwnership(); + + sharesFactory.queueMigrateYield(address(aaveYieldAggregator)); + vm.warp(block.timestamp + 4 days); + sharesFactory.executeMigrateYield(); + + vm.stopPrank(); + } + + // common set curve functions, call if need + function createPresetCurveTypes() public { + vm.startPrank(FACTORY_OWNER); + sharesFactory.setCurveType( + STANDARD_CURVE_TYPE, + STANDARD_CURVE_PARAMS.basePrice, + STANDARD_CURVE_PARAMS.inflectionPoint, + STANDARD_CURVE_PARAMS.inflectionPrice, + STANDARD_CURVE_PARAMS.linearPriceSlope + ); + + sharesFactory.setCurveType( + EXCLUSIVE_CURVE_TYPE, + EXCLUSIVE_CURVE_PARAMS.basePrice, + EXCLUSIVE_CURVE_PARAMS.inflectionPoint, + EXCLUSIVE_CURVE_PARAMS.inflectionPrice, + EXCLUSIVE_CURVE_PARAMS.linearPriceSlope + ); + vm.stopPrank(); + } + + // create buy histories as fixtures + function createPresetBuyHistory(uint8 _tradeCount) public { + uint8 tradeCount = 0; + if (_tradeCount > 0) { + tradeCount = _tradeCount; + } + + vm.deal(DAILY_TRADER, 100 ether); + vm.deal(LONG_TERM_TRADER, 100 ether); + + vm.prank(DAILY_TRADER); + sharesFactory.mintShare(DEFAULT_CURVE_TYPE); + uint256 shareId = sharesFactory.shareIndex() - 1; + + for (uint32 i = 0; i < tradeCount; i++) { + _buyShare(DAILY_TRADER, shareId, 1, SHARES_FOUNDER); + } + + uint256 currentBlockTime = block.timestamp; + vm.warp(currentBlockTime + 10 minutes); + + for (uint32 i = 0; i < tradeCount; i++) { + _buyShare(LONG_TERM_TRADER, shareId, 1, SHARES_FOUNDER); + } + + console.log("[DEBUG]: After buy times %s, DAILY_TRADER balance", tradeCount, DAILY_TRADER.balance); + console.log("[DEBUG]: After buy times %s, DAILY_TRADER balance", tradeCount, LONG_TERM_TRADER.balance); + } + + // common initial contract with specific args + // @dev call after deployContracts and if needed + + function _buyShare(address sender, uint256 shareId, uint32 quantity, address referral) internal { + (uint256 buyPriceAfterFee,,,) = sharesFactory.getBuyPriceAfterFee(shareId, quantity, referral); + + vm.prank(address(sender)); + sharesFactory.buyShare{ value: buyPriceAfterFee }(shareId, quantity, referral); + } + + function _sellShare(address sender, uint256 shareId, uint32 quantity, address referral) internal { + (uint256 sellPriceAfterFee,,,) = sharesFactory.getSellPriceAfterFee(shareId, quantity, referral); + + vm.prank(address(sender)); + sharesFactory.sellShare(shareId, quantity, sellPriceAfterFee, referral); + } + + function test_success() public { } + + function _logToFile(string memory _path, string memory _messages) internal { + vm.writeLine(_path, _messages); + } +} diff --git a/test/integration/BaseInvariantTest.t.sol b/test/integration/BaseInvariantTest.t.sol new file mode 100644 index 0000000..9dbadd1 --- /dev/null +++ b/test/integration/BaseInvariantTest.t.sol @@ -0,0 +1,116 @@ +pragma solidity ^0.8.25; + +import { console } from "forge-std/console.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SharesFactoryV1 } from "contracts/core/SharesFactoryV1.sol"; +import { SharesERC1155 } from "contracts/core/SharesERC1155.sol"; +import { AaveYieldAggregator } from "contracts/core/aggregator/AaveYieldAggregator.sol"; +import { BlankYieldAggregator } from "contracts/core/aggregator/BlankYieldAggregator.sol"; +import { IAavePool } from "contracts/interface/IAave.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { StdInvariant } from "forge-std/StdInvariant.sol"; +import { BoundedIntegrationContextHandler } from "./handlers/BoundedIntegrationContextHandler.t.sol"; + +contract BaseInvariantTest is StdInvariant { + // @dev exclude contracts for invariant testing , need to be initialized and use manual addr + address public SHARES_FOUNDER = address(1); + address public FACTORY_OWNER = address(2); + + SharesERC1155 public sharesNFT; + SharesFactoryV1 public sharesFactory; + + AaveYieldAggregator public aaveYieldAggregator; + BlankYieldAggregator public blankYieldAggregator; + + address public constant WETH = 0x4200000000000000000000000000000000000006; + address public constant AAVE_POOL = 0x794a61358D6845594F94dc1DB02A252b5b4814aD; + address public constant AAVE_WETH_GATEWAY = 0xe9E52021f4e11DEAD8661812A0A6c8627abA2a54; + + IERC20 public aWETH = IERC20(IAavePool(AAVE_POOL).getReserveData(WETH).aTokenAddress); + + string public constant BASE_URI = "https://foo.com/shares/uri/"; + + struct PresetCurveParams { + uint96 basePrice; + uint32 inflectionPoint; + uint128 inflectionPrice; + uint128 linearPriceSlope; + } + + PresetCurveParams public DEFAULT_CURVE_PARAMS = PresetCurveParams({ + basePrice: 0.005 ether, + inflectionPoint: 1500, + inflectionPrice: 0.01025 ether, + linearPriceSlope: 0 + }); + + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + // @dev target handlers for invariant testing + BoundedIntegrationContextHandler public integrationContext; + + // helper functions + // common deploy contracts with args, call if needed + function deployContracts() public { + vm.startPrank(FACTORY_OWNER); + sharesNFT = new SharesERC1155(BASE_URI); + + sharesFactory = new SharesFactoryV1( + address(sharesNFT), + DEFAULT_CURVE_PARAMS.basePrice, + DEFAULT_CURVE_PARAMS.inflectionPoint, + DEFAULT_CURVE_PARAMS.inflectionPrice, + DEFAULT_CURVE_PARAMS.linearPriceSlope + ); + + aaveYieldAggregator = new AaveYieldAggregator(address(sharesFactory), WETH, AAVE_POOL, AAVE_WETH_GATEWAY); + blankYieldAggregator = new BlankYieldAggregator(address(sharesFactory), WETH); + + sharesNFT.setFactory(address(sharesFactory)); + sharesFactory.queueMigrateYield(address(aaveYieldAggregator)); + vm.warp(block.timestamp + sharesFactory.TIMELOCK_DURATION()); + sharesFactory.executeMigrateYield(); + + sharesNFT.transferOwnership(FACTORY_OWNER); + sharesFactory.transferOwnership(FACTORY_OWNER); + aaveYieldAggregator.transferOwnership(FACTORY_OWNER); + vm.stopPrank(); + } + + function excludeDeployedContracts() public { + excludeContract(address(sharesNFT)); + excludeContract(address(sharesFactory)); + excludeContract(address(aaveYieldAggregator)); + excludeContract(address(blankYieldAggregator)); + + excludeContract(address(aWETH)); + excludeContract(address(SHARES_FOUNDER)); + excludeContract(address(FACTORY_OWNER)); + } + + function setUp() public { + deployContracts(); + excludeDeployedContracts(); + + integrationContext = new BoundedIntegrationContextHandler(FACTORY_OWNER, sharesFactory, sharesNFT); + targetContract(address(integrationContext)); + } + + function invariant_depositedETHAmount() public view { + vm.assertEq(sharesFactory.depositedETHAmount(), aWETH.balanceOf(address(aaveYieldAggregator))); + } + + function invariant_logSummary() public view { + console.log("\n"); + console.log("Log Summary: "); + + console.log( + "Num calls: boundedIntContext.setCurveType: ", integrationContext.numCalls("boundedIntContext.setCurveType") + ); + console.log("Num calls: boundedIntContext.buy: ", integrationContext.numCalls("boundedIntContext.buy")); + console.log("Num calls: boundedIntContext.test: ", integrationContext.numCalls("boundedIntContext.test")); + } + + function test_success() public pure { + vm.assertTrue(true); + } +} diff --git a/test/integration/DEVELOPMENT.md b/test/integration/DEVELOPMENT.md new file mode 100644 index 0000000..f23ae94 --- /dev/null +++ b/test/integration/DEVELOPMENT.md @@ -0,0 +1,10 @@ +# Integration Development + +1. define test cases in btt style in `.tree` files. You can use **online ascii tree editor** to create the tree files. +2. incrementally generate the test cases by running `bulloak scaffold -w test/integration/**/*.tree` command. (Make sure you have installed `bulloak`) + + + +## References + +- [bulloak](https://github.com/alexfertel/bulloak) \ No newline at end of file diff --git a/test/integration/IncomeSimulator.t.sol b/test/integration/IncomeSimulator.t.sol new file mode 100644 index 0000000..e31b7cd --- /dev/null +++ b/test/integration/IncomeSimulator.t.sol @@ -0,0 +1,306 @@ +pragma solidity ^0.8.25; + +import { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SharesFactoryV1 } from "contracts/core/SharesFactoryV1.sol"; +import { SharesERC1155 } from "contracts/core/SharesERC1155.sol"; +import { AaveYieldAggregator } from "contracts/core/aggregator/AaveYieldAggregator.sol"; +import { BlankYieldAggregator } from "contracts/core/aggregator/BlankYieldAggregator.sol"; +import { IAavePool } from "contracts/interface/IAave.sol"; +import { BaseIntegrationTest } from "./BaseIntegrationTest.t.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { LogUtil } from "../lib/LogUtil.t.sol"; + +contract IncomeSimulator is BaseIntegrationTest, LogUtil { + using SafeMath for uint256; + using Strings for uint256; + using Strings for uint128; + using Strings for uint96; + using Strings for uint32; + using Strings for uint8; + + uint128 public constant TRADER_BALANCE = 100000 ether; + + uint256 public constant BUY_DURATION = 0.2 days; + uint256 public constant YIELD_DURATION = 365 days; + uint256 public constant TARGET_INCOME = 1.5 ether; + uint32 public constant TARGET_SUPPLY = 1000; + + uint256 public constant YIELD_BUFFER = 0.05 ether; + + uint8 public curveIndex = 0; + + function setUp() public { + deployContracts(); + } + + function testFuzz_SimulateLinearCurveParams(uint256 _seed) public { + uint96 minBasePrice = 0.02 ether; + uint96 maxBasePrice = 0.1 ether; + + uint128 minLinearPriceSlope = 0; + uint128 maxLinearPriceSlope = 100000; + + // random params + uint96 basePrice = uint96(_random96(_seed, minBasePrice, maxBasePrice)); + uint128 linearPriceSlope = uint128(_random128(_seed, minLinearPriceSlope, maxLinearPriceSlope)); + + _simulateLinearCurveParams(basePrice, linearPriceSlope); + } + + function testFuzz_SimulateSigmoidCurveParams(uint256 _seed) public { + uint96 minBasePrice = 0.02 ether; + uint96 maxBasePrice = 0.1 ether; + + uint128 minLinearPriceSlope = 0; + uint128 maxLinearPriceSlope = 100000; + + uint32 minInflectionPoint = 100; + uint32 maxInflectionPoint = 1000; + + uint128 minInflectionPrice = 0.02 ether; + uint128 maxInflectionPrice = 0.05 ether; + + // random params + uint96 basePrice = uint96(_random96(_seed, minBasePrice, maxBasePrice)); + uint128 linearPriceSlope = uint128(_random128(_seed, minLinearPriceSlope, maxLinearPriceSlope)); + uint32 inflectionPoint = uint32(_random32(_seed, minInflectionPoint, maxInflectionPoint)); + uint128 inflectionPrice = uint128(_random128(_seed, minInflectionPrice, maxInflectionPrice)); + + _simulateSigmoidCurveParams(basePrice, linearPriceSlope, inflectionPoint, inflectionPrice); + } + + function testFuzz_SimulateExclusiveSigmoidCurveParams(uint256 _seed) public { + // @dev exclusive sigmoid curve params has extreme high inflectionPoint + uint96 minBasePrice = 0.015 ether; + uint96 maxBasePrice = 0.1 ether; + + uint128 minLinearPriceSlope = 0; + uint128 maxLinearPriceSlope = 100000; + + uint32 minInflectionPoint = 100; + uint32 maxInflectionPoint = 1000; + + uint128 minInflectionPrice = 0.02 ether; + uint128 maxInflectionPrice = 0.1 ether; + + // random params + uint96 basePrice = uint96(_random96(_seed, minBasePrice, maxBasePrice)); + uint128 linearPriceSlope = uint128(_random128(_seed, minLinearPriceSlope, maxLinearPriceSlope)); + uint32 inflectionPoint = uint32(_random32(_seed, minInflectionPoint, maxInflectionPoint)); + uint128 inflectionPrice = uint128(_random128(_seed, minInflectionPrice, maxInflectionPrice)); + + _simulateExclusiveSigmoidCurveParams(basePrice, linearPriceSlope, inflectionPoint, inflectionPrice); + } + + function _simulateLinearCurveParams(uint96 _basePrice, uint128 _linearPriceSlope) public { + deployContracts(); // redeploy contracts + + (uint256 income, uint8 reachTargetInDays) = _simulateClaimableAmountAndFee( + _basePrice, _linearPriceSlope, 0, 0, TARGET_SUPPLY, BUY_DURATION, YIELD_DURATION + ); + console.log("Reach target in days: ", reachTargetInDays, "Income: ", income); + + if (income < TARGET_INCOME) return; + + _logSummary( + "linear_curve_params", + _contactString( + "LinearCurveParams: ", + _basePrice.toString(), + ",", + _linearPriceSlope.toString(), + ", ReachTargetInDays: ", + reachTargetInDays.toString(), + ", Income: ", + income.toString() + ) + ); + } + + function _simulateSigmoidCurveParams( + uint96 _basePrice, + uint128 _linearPriceSlope, + uint32 _inflectionPoint, + uint128 _inflectionPrice + ) public { + deployContracts(); // redeploy contracts + + (uint256 income, uint8 reachTargetInDays) = _simulateClaimableAmountAndFee( + _basePrice, + _linearPriceSlope, + _inflectionPoint, + _inflectionPrice, + TARGET_SUPPLY, + BUY_DURATION, + YIELD_DURATION + ); + console.log("Reach target in days: ", reachTargetInDays, "Income: ", income); + + if (income < TARGET_INCOME) return; + + string memory params = _contactString( + "SigmoidCurveParams: ", + _basePrice.toString(), + ",", + _linearPriceSlope.toString(), + ",", + _inflectionPoint.toString(), + ",", + _inflectionPrice.toString() + ); + string memory results = + _contactString(", ReachTargetInDays: ", reachTargetInDays.toString(), ", Income: ", income.toString()); + _logSummary("sigmoid_curve_params", _contactString(params, results)); + } + + function _simulateExclusiveSigmoidCurveParams( + uint96 _basePrice, + uint128 _linearPriceSlope, + uint32 _inflectionPoint, + uint128 _inflectionPrice + ) public { + deployContracts(); // redeploy contracts + + (uint256 income, uint8 reachTargetInDays) = _simulateClaimableAmountAndFee( + _basePrice, + _linearPriceSlope, + _inflectionPoint, + _inflectionPrice, + TARGET_SUPPLY, + BUY_DURATION, + YIELD_DURATION + ); + console.log("Reach target in days: ", reachTargetInDays, "Income: ", income); + + if (income < TARGET_INCOME) return; + string memory params = _contactString( + "ExclusiveSigmoidCurveParams: ", + _basePrice.toString(), + ",", + _linearPriceSlope.toString(), + ",", + _inflectionPoint.toString(), + ",", + _inflectionPrice.toString() + ); + string memory results = + _contactString(", ReachTargetInDays: ", reachTargetInDays.toString(), ", Income: ", income.toString()); + _logSummary("exclusive_sigmoid_curve_params", _contactString(params, results)); + } + + function _simulateClaimableAmountAndFee( + // curve params + uint96 _basePrice, + uint128 _linearPriceSlope, + uint32 _inflectionPoint, + uint128 _inflectionPrice, + // target supply + uint32 _targetSupply, + // buy duration + uint256 _buyDuration, + // yield duration + uint256 _yieldDuration + ) public returns (uint256 income, uint8 reachTargetInDays) { + reachTargetInDays = 0; + + uint8 curveType = curveIndex + 1; + curveIndex++; + // create and set curve type + vm.prank(FACTORY_OWNER); + sharesFactory.setCurveType(curveType, _basePrice, _inflectionPoint, _inflectionPrice, _linearPriceSlope); + + // mint shares + vm.prank(FACTORY_OWNER); + sharesFactory.mintShare(curveType); + + // record start balance + uint256 founderBalanceBefore = aWETH.balanceOf(address(SHARES_FOUNDER)); + console.log("Founder Start Balance: ", founderBalanceBefore); + + // buy shares + vm.deal(DAILY_TRADER, TRADER_BALANCE); + uint256 startBlockTime = block.timestamp; + for (uint32 i = 0; i < _targetSupply; i++) { + vm.warp(startBlockTime + i * _buyDuration); + _buyShare(DAILY_TRADER, sharesFactory.shareIndex() - 1, 1, SHARES_FOUNDER); + + // check max claimable amount + uint256 currentClaimableAmount = _checkClaimableAmountAndFee(); + // _logSummary(_contactString("Total supply: ", i.toString(), ", CurrentClaimableAmount:", currentClaimableAmount.toString())); + // once claimableAmount > targetIncome + if (currentClaimableAmount > TARGET_INCOME && reachTargetInDays == 0) { + reachTargetInDays = uint8((i * _buyDuration) / (1 days)); + console.log("This params earn income after days: ", reachTargetInDays); + } + } + + // time flies + vm.assertTrue(_yieldDuration > _buyDuration * _targetSupply); + + // after soling TARGET_SUPPLY waiting for time files and check claimableAmount + uint256 finalYieldDuration = _yieldDuration - _buyDuration * _targetSupply; + uint256 checkPointDuration = 1 days; + uint32 checkTime = uint32(finalYieldDuration / checkPointDuration); + + for (uint32 i = 0; i < checkTime; i++) { + vm.warp(startBlockTime + _buyDuration * _targetSupply + i * checkPointDuration); + // check max claimable amount + uint256 currentClaimableAmount = _checkClaimableAmountAndFee(); + + // once claimableAmount > targetIncome + if (currentClaimableAmount > TARGET_INCOME && reachTargetInDays == 0) { + reachTargetInDays = uint8((i * _buyDuration) / (1 days)); + console.log("This params earn income after days: ", reachTargetInDays); + } + } + + uint256 claimableAmountAndFee = _checkClaimableAmountAndFee(); + + income = claimableAmountAndFee; + } + + function _checkClaimableAmountAndFee() public returns (uint256 v) { + vm.prank(FACTORY_OWNER); + uint256 depositedETHAmount = sharesFactory.depositedETHAmount(); + uint256 claimableAmount = aaveYieldAggregator.yieldMaxClaimable(depositedETHAmount); + uint256 founderBalance = aWETH.balanceOf(SHARES_FOUNDER); + + v = claimableAmount + founderBalance; + return v; + } + + function _logSummary(string memory _filename, string memory _content) internal { + string memory filename = _contactString("reports/", _filename, ".txt"); + _logToFile(filename, _content); + } + + function _random(uint256 _seed, uint256 min, uint256 max) public view returns (uint256) { + require(max > min, "max must be greater than min"); + uint256 randomHash = uint256(keccak256(abi.encodePacked(_seed, block.timestamp, msg.sender))); + return (randomHash % (max - min + 1)) + min; + } + + function _random32(uint256 _seed, uint32 min, uint32 max) public view returns (uint32) { + require(max > min, "max must be greater than min"); + return uint32(_random(_seed, min, max) % (2**32)); + } + + function _random96(uint256 _seed, uint96 min, uint96 max) public view returns (uint96) { + require(max > min, "max must be greater than min"); + return uint96(_random(_seed, min, max) % (2**96)); + } + + function _random128(uint256 _seed, uint128 min, uint128 max) public view returns (uint128) { + require(max > min, "max must be greater than min"); + return uint128(_random(_seed, min, max) % (2**128)); + } + + // TODO: + function _logGraphData(string memory _filename, uint _x, uint _y) internal { + string memory filename = _contactString("reports/", _filename, ".txt"); + _logToFile(filename, _contactString(_x.toString(), ",", _y.toString())); + } +} diff --git a/test/integration/handlers/BoundedIntegrationContextHandler.t.sol b/test/integration/handlers/BoundedIntegrationContextHandler.t.sol new file mode 100644 index 0000000..dd654be --- /dev/null +++ b/test/integration/handlers/BoundedIntegrationContextHandler.t.sol @@ -0,0 +1,69 @@ +pragma solidity ^0.8.25; + +import { StdUtils } from "forge-std/StdUtils.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { SharesFactoryV1 } from "contracts/core/SharesFactoryV1.sol"; +import { SharesERC1155 } from "contracts/core/SharesERC1155.sol"; + +contract BoundedIntegrationContextHandler is StdUtils { + mapping(bytes32 => uint256) public numCalls; + + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + address public factoryOwner; + SharesERC1155 public sharesNFT; + SharesFactoryV1 public sharesFactory; + + address public constant FACTORY_OWNER = address(1); + + address public constant TRADER = address(5); + + // @dev internal helper state for bounded range calculation + mapping(uint8 => bool) public usedCurveType; + + constructor(address _factoryOwner, SharesFactoryV1 _sharesFactory, SharesERC1155 _sharesNFT) { + factoryOwner = _factoryOwner; + sharesFactory = SharesFactoryV1(_sharesFactory); + sharesNFT = SharesERC1155(_sharesNFT); + } + + function setCurveType( + uint8 _curveType, + uint96 _basePrice, + uint32 _inflectionPoint, + uint128 _inflectionPrice, + uint128 _linearPriceSlope + ) public { + numCalls["boundedIntContext.setCurveType"]++; + + vm.assume(!usedCurveType[_curveType]); + bound(_curveType, 0, 100); + bound(_basePrice, 0.005 ether, 0.1 ether); + bound(_inflectionPoint, 1000, 2000); + bound(_inflectionPrice, 0.01 ether, 0.5 ether); + bound(_linearPriceSlope, 0, 0); + + usedCurveType[_curveType] = true; + vm.prank(factoryOwner); + sharesFactory.setCurveType(_curveType, _basePrice, _inflectionPoint, _inflectionPrice, _linearPriceSlope); + } + + function buy(uint32 quantity) public { + numCalls["boundedIntContext.buy"]++; + + bound(quantity, 1, 1000); + + uint256 shareId = 0; + + vm.prank(factoryOwner); + sharesFactory.mintShare(0); + + vm.prank(TRADER); + + (uint256 buyPriceAfterFee,,,) = sharesFactory.getBuyPriceAfterFee(shareId, quantity, FACTORY_OWNER); + vm.prank(TRADER); + sharesFactory.buyShare{ value: buyPriceAfterFee }(shareId, quantity, FACTORY_OWNER); + } + + function test() public { } +} diff --git a/test/lib/LogUtil.t.sol b/test/lib/LogUtil.t.sol new file mode 100644 index 0000000..4d58cac --- /dev/null +++ b/test/lib/LogUtil.t.sol @@ -0,0 +1,132 @@ +pragma solidity ^0.8.25; + +import { Test } from "forge-std/Test.sol"; + +contract LogUtil is Test { + function _contactString(string memory a, string memory b) internal pure returns (string memory) { + return string(abi.encodePacked(a, b)); + } + + function _contactString(string memory a, string memory b, string memory c) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d, + string memory e + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d, e)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d, + string memory e, + string memory f + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d, e, f)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d, + string memory e, + string memory f, + string memory g + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d, e, f, g)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d, + string memory e, + string memory f, + string memory g, + string memory h + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d, e, f, g, h)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d, + string memory e, + string memory f, + string memory g, + string memory h, + string memory i + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d, e, f, g, h, i)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d, + string memory e, + string memory f, + string memory g, + string memory h, + string memory i, + string memory j + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d, e, f, g, h, i, j)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d, + string memory e, + string memory f, + string memory g, + string memory h, + string memory i, + string memory j, + string memory k + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d, e, f, g, h, i, j, k)); + } + + function _contactString( + string memory a, + string memory b, + string memory c, + string memory d, + string memory e, + string memory f, + string memory g, + string memory h, + string memory i, + string memory j, + string memory k, + string memory l + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b, c, d, e, f, g, h, i, j, k, l)); + } + + function test() public {} +}