From 1b5b5f6695cf52bb5aa0c19bb3b855e1f59a4550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87a=C4=9Fla=20=C3=87elik?= Date: Tue, 3 Dec 2024 20:06:55 +0300 Subject: [PATCH] added more tests + ref --- .vscode => .vscode/settings.json | 0 Makefile | 12 +- README.md | 7 +- foundry.toml | 15 +- lib/dria-oracle-contracts | 2 +- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- lib/openzeppelin-contracts-upgradeable | 2 +- lib/openzeppelin-foundry-upgrades | 2 +- script/Deploy.s.sol | 16 +- script/HelperConfig.s.sol | 14 +- src/BuyerAgent.sol | 24 +++ src/Swan.sol | 18 +- test/Helper.t.sol | 39 ++++- test/Invariant.t.sol | 51 ++++++ test/Swan.t.sol | 33 +++- test/SwanFuzz.t.sol | 217 +++++++++++++++++++++++++ test/SwanIntervals.t.sol | 95 ----------- 18 files changed, 422 insertions(+), 129 deletions(-) rename .vscode => .vscode/settings.json (100%) create mode 100644 test/Invariant.t.sol create mode 100644 test/SwanFuzz.t.sol diff --git a/.vscode b/.vscode/settings.json similarity index 100% rename from .vscode rename to .vscode/settings.json diff --git a/Makefile b/Makefile index c16d2e4..178657f 100644 --- a/Makefile +++ b/Makefile @@ -28,13 +28,17 @@ update: build: forge clean && forge build -# Generate gas snapshot under snapshots directory +# Generate gas snapshot snapshot: forge snapshot -# Test the contracts on forked base-sepolia network +# Test the contracts forked base-sepolia network with 4 parallel jobs test: - forge clean && forge test --fork-url $(BASE_TEST_RPC_URL) + forge clean && forge test --fork-url $(BASE_TEST_RPC_URL) --no-match-contract "InvariantTest" --jobs 4 + +# Run invariant tests on local network with 4 parallel jobs +test-inv: + forge clean && forge test --match-contract "InvariantTest" --jobs 4 anvil: anvil --fork-url $(BASE_TEST_RPC_URL) @@ -57,7 +61,7 @@ fmt: # Coverage cov: - forge coverage --no-match-coverage "(test|mock|script)" + forge clean && forge coverage --no-match-coverage "(test|mock|script)" --no-match-contract "InvariantTest" --jobs 4 # Verify contract on blockscout verify: diff --git a/README.md b/README.md index c7940a0..10ed460 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,17 @@ make build ## Test -Run tests on forked base-sepolia: +Run tests on forked base-sepolia with: ```sh make test ``` +Run invariant tests on local with: +```sh +make test-inv +``` + ## Deployment **Step 1.** diff --git a/foundry.toml b/foundry.toml index f00ffe9..1c27652 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,11 +5,23 @@ test = 'test' script = 'script' out = 'out' cache_path = 'cache' +extra_output = ['storageLayout'] + ffi = true ast = true build_info = true optimizer = true -extra_output = ['storageLayout'] + +# fuzzing options +fuzz_runs = 100 + +# invariant options +invariant_runs = 20 + +# block timestamp starts from 10 +block_timestamp = 10 + +# fs permissions for deployment fs_permissions = [{ access = "read", path = "out" }, { access = "write", path = "deployment" }] remappings = [ "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", @@ -18,4 +30,5 @@ remappings = [ "@firstbatch/dria-oracle-contracts/=lib/dria-oracle-contracts/src/" ] + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/dria-oracle-contracts b/lib/dria-oracle-contracts index 85accaa..54ba49f 160000 --- a/lib/dria-oracle-contracts +++ b/lib/dria-oracle-contracts @@ -1 +1 @@ -Subproject commit 85accaaa974bae429fa571b090888e9676f62c89 +Subproject commit 54ba49f9d68ffe125f895dc1163a0d8eafbad503 diff --git a/lib/forge-std b/lib/forge-std index 83c5d21..1eea5ba 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 83c5d212a01f8950727da4095cdfe2654baccb5b +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 78be1b3..69c8def 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 78be1b39aa6860f9bada089e0b77e5e99759ca75 +Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 5fcea08..fa52531 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 5fcea080abd754c44334f50d1fb8b738a2091372 +Subproject commit fa525310e45f91eb20a6d3baa2644be8e0adba31 diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades index 8e754fd..16e0ae2 160000 --- a/lib/openzeppelin-foundry-upgrades +++ b/lib/openzeppelin-foundry-upgrades @@ -1 +1 @@ -Subproject commit 8e754fde23b2b030a35bb47cd84b77dd42a44437 +Subproject commit 16e0ae21e0e39049f619f2396fa28c57fad07368 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 77c015c..9b2ea24 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -53,7 +53,10 @@ contract Deploy is Script { // deploy llm contracts address registryProxy = Upgrades.deployUUPSProxy( "LLMOracleRegistry.sol", - abi.encodeCall(LLMOracleRegistry.initialize, (genStake, valStake, address(config.token()))) + abi.encodeCall( + LLMOracleRegistry.initialize, + (genStake, valStake, address(config.token()), config.minRegistrationTime()) + ) ); // wrap proxy with the LLMOracleRegistry @@ -65,7 +68,15 @@ contract Deploy is Script { "LLMOracleCoordinator.sol", abi.encodeCall( LLMOracleCoordinator.initialize, - (address(oracleRegistry), address(config.token()), platformFee, genFee, valFee) + ( + address(oracleRegistry), + address(config.token()), + platformFee, + genFee, + valFee, + config.minScore(), + config.maxScore() + ) ) ); @@ -87,6 +98,7 @@ contract Deploy is Script { uint256 platformFee, uint256 maxAssetCount, uint256 minAssetPrice, + /* timestamp */ , uint8 maxBuyerAgentFee ) = config.marketParams(); diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol index b2dbd54..1c76829 100644 --- a/script/HelperConfig.s.sol +++ b/script/HelperConfig.s.sol @@ -13,8 +13,8 @@ struct Stakes { struct Fees { uint256 platformFee; - uint256 generatorFee; - uint256 validatorFee; + uint256 generationFee; + uint256 validationFee; } contract HelperConfig is Script { @@ -25,10 +25,14 @@ contract HelperConfig is Script { Fees public fees; WETH9 public token; + uint256 public minRegistrationTime; // in seconds + uint256 public minScore; + uint256 public maxScore; + constructor() { // set deployment parameters stakes = Stakes({generatorStakeAmount: 0.0001 ether, validatorStakeAmount: 0.000001 ether}); - fees = Fees({platformFee: 0.0001 ether, generatorFee: 0.0001 ether, validatorFee: 0.0001 ether}); + fees = Fees({platformFee: 0.0001 ether, generationFee: 0.0001 ether, validationFee: 0.0001 ether}); taskParams = LLMOracleTaskParameters({difficulty: 2, numGenerations: 1, numValidations: 1}); marketParams = SwanMarketParameters({ @@ -42,6 +46,10 @@ contract HelperConfig is Script { maxBuyerAgentFee: 75 // percentage }); + minRegistrationTime = 1 days; + maxScore = type(uint8).max; // 255 + minScore = 1; + // for base sepolia if (block.chainid == 84532) { // use deployed weth diff --git a/src/BuyerAgent.sol b/src/BuyerAgent.sol index 087b521..628ca9c 100644 --- a/src/BuyerAgent.sol +++ b/src/BuyerAgent.sol @@ -47,6 +47,22 @@ contract BuyerAgent is Ownable { /// @notice The task was already processed, via `purchase` or `updateState`. error TaskAlreadyProcessed(); + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a state update request is made. + event StateRequest(uint256 indexed taskId, uint256 indexed round); + + /// @notice Emitted when a purchase request is made. + event PurchaseRequest(uint256 indexed taskId, uint256 indexed round); + + /// @notice Emitted when a purchase is made. + event Purchase(uint256 indexed round); + + /// @notice Emitted when the state is updated. + event StateUpdate(uint256 indexed round); + /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ @@ -178,6 +194,8 @@ contract BuyerAgent is Ownable { oracleStateRequests[round] = swan.coordinator().request(SwanBuyerStateOracleProtocol, _input, _models, swan.getOracleParameters()); + + emit StateRequest(oracleStateRequests[round], round); } /// @notice Calls the LLMOracleCoordinator & pays for the oracle fees to make a purchase request. @@ -193,6 +211,8 @@ contract BuyerAgent is Ownable { oraclePurchaseRequests[round] = swan.coordinator().request(SwanBuyerPurchaseOracleProtocol, _input, _models, swan.getOracleParameters()); + + emit PurchaseRequest(oraclePurchaseRequests[round], round); } /// @notice Function to update the Buyer state. @@ -214,6 +234,8 @@ contract BuyerAgent is Ownable { // update taskId as completed isOracleRequestProcessed[taskId] = true; + + emit StateUpdate(round); } /// @notice Function to buy the asset from the Swan with the given assed address. @@ -254,6 +276,8 @@ contract BuyerAgent is Ownable { // update taskId as completed isOracleRequestProcessed[taskId] = true; + + emit Purchase(round); } /// @notice Function to withdraw the tokens from the contract. diff --git a/src/Swan.sol b/src/Swan.sol index 8411507..17c8649 100644 --- a/src/Swan.sol +++ b/src/Swan.sol @@ -4,18 +4,21 @@ pragma solidity ^0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import {LLMOracleCoordinator} from "@firstbatch/dria-oracle-contracts/LLMOracleCoordinator.sol"; -import {LLMOracleTaskParameters} from "@firstbatch/dria-oracle-contracts/LLMOracleTask.sol"; +import {LLMOracleCoordinator, LLMOracleTaskParameters} from "@firstbatch/dria-oracle-contracts/LLMOracleCoordinator.sol"; import {BuyerAgentFactory, BuyerAgent} from "./BuyerAgent.sol"; import {SwanAssetFactory, SwanAsset} from "./SwanAsset.sol"; import {SwanManager, SwanMarketParameters} from "./SwanManager.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -// Protocol strings for Swan, checked in the Oracle. +// @dev Protocol strings for Swan, checked in the Oracle. bytes32 constant SwanBuyerPurchaseOracleProtocol = "swan-buyer-purchase/0.1.0"; bytes32 constant SwanBuyerStateOracleProtocol = "swan-buyer-state/0.1.0"; + +/// @dev Used to calculate the fee for the buyer agent to be able to compute correct amount. uint256 constant BASIS_POINTS = 10_000; contract Swan is SwanManager, UUPSUpgradeable { + using Math for uint256; /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -307,15 +310,16 @@ contract Swan is SwanManager, UUPSUpgradeable { /// @notice Function to transfer the royalties to the seller & Dria. function transferRoyalties(AssetListing storage asset) internal { // calculate fees - uint256 buyerFee = (asset.price * asset.feeRoyalty) / (BASIS_POINTS * BASIS_POINTS); - uint256 driaFee = (buyerFee * asset.feeRoyalty * getCurrentMarketParameters().platformFee) / (BASIS_POINTS * BASIS_POINTS); + uint256 totalFee = Math.mulDiv(asset.price, (asset.feeRoyalty * 100), BASIS_POINTS); + uint256 driaFee = Math.mulDiv(totalFee, (getCurrentMarketParameters().platformFee * 100), BASIS_POINTS); + uint256 buyerFee = totalFee - driaFee; // first, Swan receives the entire fee from seller // this allows only one approval from the seller's side - token.transferFrom(asset.seller, address(this), buyerFee); + token.transferFrom(asset.seller, address(this), totalFee); // send the buyer's portion to them - token.transfer(asset.buyer, buyerFee - driaFee); + token.transfer(asset.buyer, buyerFee); // then it sends the remaining to Swan owner token.transfer(owner(), driaFee); diff --git a/test/Helper.t.sol b/test/Helper.t.sol index f93fc96..2b7f3fb 100644 --- a/test/Helper.t.sol +++ b/test/Helper.t.sol @@ -65,13 +65,17 @@ abstract contract Helper is Test { uint256 amountPerRound = 0.015 ether; uint8 feeRoyalty = 2; - // Default scores for validation + /// @dev Default scores for validation uint256[] scores = [1, 5, 70]; + uint256 public minRegistrationTime = 1 days; // in seconds + uint256 public minScore = 1; + uint256 public maxScore = type(uint8).max; // 255 + /// @notice The given nonce is not a valid proof-of-work. error InvalidNonceFromHelperTest(uint256 taskId, uint256 nonce, uint256 computedNonce, address caller); - // Set parameters for the test + // @dev Set parameters for the test function setUp() public deployment { dria = vm.addr(1); validators = [vm.addr(2), vm.addr(3), vm.addr(4)]; @@ -79,7 +83,7 @@ abstract contract Helper is Test { buyerAgentOwners = [vm.addr(8), vm.addr(9)]; sellers = [vm.addr(10), vm.addr(11)]; - oracleParameters = LLMOracleTaskParameters({difficulty: 1, numGenerations: 1, numValidations: 1}); + oracleParameters = LLMOracleTaskParameters({difficulty: 1, numGenerations: 2, numValidations: 1}); marketParameters = SwanMarketParameters({ withdrawInterval: 300, // 5 minutes sellInterval: 360, @@ -92,7 +96,7 @@ abstract contract Helper is Test { }); stakes = Stakes({generatorStakeAmount: 0.01 ether, validatorStakeAmount: 0.01 ether}); - fees = Fees({platformFee: 0.0001 ether, generatorFee: 0.0002 ether, validatorFee: 0.00003 ether}); + fees = Fees({platformFee: 1, generationFee: 0.0002 ether, validationFee: 0.00003 ether}); for (uint96 i = 0; i < buyerAgentOwners.length; i++) { buyerAgentParameters.push( @@ -125,7 +129,8 @@ abstract contract Helper is Test { address registryProxy = Upgrades.deployUUPSProxy( "LLMOracleRegistry.sol", abi.encodeCall( - LLMOracleRegistry.initialize, (stakes.generatorStakeAmount, stakes.validatorStakeAmount, address(token)) + LLMOracleRegistry.initialize, + (stakes.generatorStakeAmount, stakes.validatorStakeAmount, address(token), minRegistrationTime) ) ); oracleRegistry = LLMOracleRegistry(registryProxy); @@ -134,7 +139,15 @@ abstract contract Helper is Test { "LLMOracleCoordinator.sol", abi.encodeCall( LLMOracleCoordinator.initialize, - (address(oracleRegistry), address(token), fees.platformFee, fees.generatorFee, fees.validatorFee) + ( + address(oracleRegistry), + address(token), + fees.platformFee, + fees.generationFee, + fees.validationFee, + minScore, + maxScore + ) ) ); oracleCoordinator = LLMOracleCoordinator(coordinatorProxy); @@ -169,6 +182,17 @@ abstract contract Helper is Test { vm.label(address(swanAssetFactory), "SwanAssetFactory"); } + /// @notice Add validators to the whitelist. + modifier addValidatorsToWhitelist() { + vm.prank(dria); + oracleRegistry.addToWhitelist(validators); + + for (uint256 i; i < validators.length; i++) { + vm.assertTrue(oracleRegistry.whitelisted(validators[i])); + } + _; + } + /// @notice Register generators and validators modifier registerOracles() { for (uint256 i = 0; i < generators.length; i++) { @@ -460,11 +484,12 @@ abstract contract Helper is Test { // respond safeRespond(generators[0], encodedOutput, taskId); + safeRespond(generators[1], encodedOutput, taskId); // validate safeValidate(validators[0], taskId); - assert(token.balanceOf(address(buyerAgent)) > assetPrice); + assertGe(token.balanceOf(address(buyerAgent)), assetPrice); // purchase and check event logs vm.recordLogs(); diff --git a/test/Invariant.t.sol b/test/Invariant.t.sol new file mode 100644 index 0000000..fa3805c --- /dev/null +++ b/test/Invariant.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {Helper} from "./Helper.t.sol"; +import {SwanMarketParameters} from "../src/Swan.sol"; + +/// @notice Invariant tests call random functions from contracts and check the conditions inside the test +contract InvariantTest is Helper { + // Owner is always an operator + function invariant_OwnerIsAnOperator() public view { + assertTrue(swan.isOperator(swan.owner())); + } + + /// @dev Total number of assets listed does not exceed maxAssetCount + function invariant_MaxAssetCount() public view { + SwanMarketParameters memory params = swan.getCurrentMarketParameters(); + + for (uint256 i = 0; i < buyerAgents.length; i++) { + for (uint256 round = 0; round < 5; round++) { + // asssuming a maximum of 5 rounds + assertTrue(swan.getListedAssets(address(buyerAgents[i]), round).length <= params.maxAssetCount); + } + } + } + + /// @dev Price of listed assets is within the acceptable range + function invariant_AssetPriceRange() public view { + SwanMarketParameters memory params = swan.getCurrentMarketParameters(); + + for (uint256 i = 0; i < buyerAgents.length; i++) { + for (uint256 round; round < 5; round++) { + // assuming a maximum of 5 rounds + address[] memory assets = swan.getListedAssets(address(buyerAgents[i]), round); + for (uint256 j; j < assets.length; j++) { + uint256 price = swan.getListingPrice(assets[j]); + assertTrue(price >= params.minAssetPrice && price <= buyerAgents[i].amountPerRound()); + } + } + } + } + + /// @dev Fee royalty of each buyer agent is within an acceptable range + function invariant_BuyerAgentFeeRoyalty() public view { + for (uint256 i = 0; i < buyerAgents.length; i++) { + uint96 feeRoyalty = buyerAgents[i].feeRoyalty(); + assertTrue(feeRoyalty >= 0 && feeRoyalty <= 10000); // Assuming fee royalty is in basis points (0% to 100%) + } + } +} diff --git a/test/Swan.t.sol b/test/Swan.t.sol index bc800a8..e86ceaf 100644 --- a/test/Swan.t.sol +++ b/test/Swan.t.sol @@ -13,11 +13,13 @@ import {LLMOracleRegistry} from "@firstbatch/dria-oracle-contracts/LLMOracleRegi import { LLMOracleCoordinator, LLMOracleTaskParameters } from "@firstbatch/dria-oracle-contracts/LLMOracleCoordinator.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {console} from "forge-std/Test.sol"; contract SwanTest is Helper { /// @dev Fund geerators, validators, sellers, and dria modifier fund() { - scores = [10]; + scores = [10, 15]; // fund dria deal(address(token), dria, 1 ether); @@ -59,6 +61,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -74,6 +77,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -91,6 +95,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -108,6 +113,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -128,6 +134,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -148,6 +155,7 @@ contract SwanTest is Helper { // respond safeRespond(generators[0], encodedOutput, 1); + safeRespond(generators[1], encodedOutput, 1); // validate safeValidate(validators[0], 1); @@ -167,6 +175,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -178,8 +187,16 @@ contract SwanTest is Helper { safePurchase(buyerAgentOwners[0], buyerAgents[0], 1); Vm.Log[] memory entries = vm.getRecordedLogs(); + // 1. Transfer + // 2. Transfer + // 3. Transfer + // 4. Transfer + // 5. AssetSold (from Swan) + // 6. Purchase (from BuyerAgent) + assertEq(entries.length, 6); + // get the AssetSold event - Vm.Log memory assetSoldEvent = entries[entries.length - 1]; + Vm.Log memory assetSoldEvent = entries[entries.length - 2]; // check event sig bytes32 eventSig = assetSoldEvent.topics[0]; @@ -187,11 +204,11 @@ contract SwanTest is Helper { // decode params from event address _seller = abi.decode(abi.encode(assetSoldEvent.topics[1]), (address)); - address agent = abi.decode(abi.encode(assetSoldEvent.topics[2]), (address)); + address _agent = abi.decode(abi.encode(assetSoldEvent.topics[2]), (address)); address asset = abi.decode(abi.encode(assetSoldEvent.topics[3]), (address)); uint256 price = abi.decode(assetSoldEvent.data, (uint256)); - assertEq(agent, address(buyerAgents[0])); + assertEq(_agent, address(buyerAgents[0])); assertEq(asset, buyerAgents[0].inventory(0, 0)); // get asset details @@ -213,6 +230,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -236,6 +254,8 @@ contract SwanTest is Helper { buyerAgent.oracleStateRequest(input, models); safeRespond(generators[0], newState, taskId); + safeRespond(generators[1], newState, taskId); + safeValidate(validators[0], taskId); vm.prank(buyerAgentOwner); @@ -310,6 +330,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -343,6 +364,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -367,6 +389,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -416,6 +439,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { @@ -441,6 +465,7 @@ contract SwanTest is Helper { fund createBuyers sellersApproveToSwan + addValidatorsToWhitelist registerOracles listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) { diff --git a/test/SwanFuzz.t.sol b/test/SwanFuzz.t.sol new file mode 100644 index 0000000..8e90b7b --- /dev/null +++ b/test/SwanFuzz.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {Helper} from "./Helper.t.sol"; + +import {BuyerAgent, BuyerAgentFactory} from "../src/BuyerAgent.sol"; +import {SwanAssetFactory, SwanAsset} from "../src/SwanAsset.sol"; +import {Swan, SwanMarketParameters} from "../src/Swan.sol"; +import {WETH9} from "./WETH9.sol"; +import {LLMOracleRegistry} from "@firstbatch/dria-oracle-contracts/LLMOracleRegistry.sol"; +import { + LLMOracleCoordinator, LLMOracleTaskParameters +} from "@firstbatch/dria-oracle-contracts/LLMOracleCoordinator.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {console} from "forge-std/Test.sol"; + +/// @notice Fuzz test is used to test call the functions with random values multiple times +contract SwanFuzz is Helper { + modifier fund() { + // fund dria + deal(address(token), dria, 1 ether); + + // fund sellers + for (uint256 i; i < sellers.length; i++) { + deal(address(token), sellers[i], 5 ether); + assertEq(token.balanceOf(sellers[i]), 5 ether); + vm.label(address(sellers[i]), string.concat("Seller#", vm.toString(i + 1))); + } + _; + } + + /// @notice Calculate royalties + function testFuzz_CalculateRoyalties(uint256 price, uint256 agentFee, uint256 driaFee) external createBuyers { + agentFee = bound(agentFee, 1, 80); + require(agentFee <= 80 && agentFee > 0, "Agent fee is not correctly set"); + + driaFee = bound(driaFee, 1, 80); + require(driaFee <= 80 && driaFee > 0, "Dria fee is not correctly set"); + + price = bound(price, 0.000001 ether, 0.2 ether); + require(price >= 0.000001 ether && price <= 0.2 ether, "Price is not correctly set"); + + uint256 expectedTotalFee = Math.mulDiv(price, (agentFee * 100), 10000); + uint256 expectedDriaFee = + Math.mulDiv(expectedTotalFee, (swan.getCurrentMarketParameters().platformFee * 100), 10000); + uint256 expectedAgentFee = expectedTotalFee - expectedDriaFee; + + assertEq(expectedAgentFee + expectedDriaFee, expectedTotalFee, "Invalid fee calculation"); + } + + /// @notice Change the intervals and check the current phase and round is are correct + function testFuzz_ChangeCycleTime( + uint256 sellIntervalForFirstSet, + uint256 buyIntervalForFirstset, + uint256 withdrawIntervalForFirstSet, + uint256 sellIntervalForSecondSet, + uint256 buyIntervalForSecondSet, + uint256 withdrawIntervalForSecondSet + ) external createBuyers { + sellIntervalForFirstSet = bound(sellIntervalForFirstSet, 15 minutes, 2 days); + require( + sellIntervalForFirstSet >= 15 minutes && sellIntervalForFirstSet <= 2 days, + "SellInterval is not correctly set" + ); + + sellIntervalForSecondSet = bound(sellIntervalForSecondSet, 15 minutes, 2 days); + require( + sellIntervalForSecondSet >= 15 minutes && sellIntervalForSecondSet <= 2 days, + "SellInterval is not correctly set" + ); + + buyIntervalForFirstset = bound(buyIntervalForFirstset, 15 minutes, 2 days); + require( + buyIntervalForFirstset >= 15 minutes && buyIntervalForFirstset <= 2 days, "BuyInterval is not correctly set" + ); + + buyIntervalForSecondSet = bound(buyIntervalForSecondSet, 15 minutes, 2 days); + require( + buyIntervalForSecondSet >= 15 minutes && buyIntervalForSecondSet <= 2 days, + "BuyInterval is not correctly set" + ); + + withdrawIntervalForFirstSet = bound(withdrawIntervalForFirstSet, 15 minutes, 2 days); + require( + withdrawIntervalForFirstSet >= 15 minutes && withdrawIntervalForFirstSet <= 2 days, + "WithdrawInterval is not correctly set" + ); + + withdrawIntervalForSecondSet = bound(withdrawIntervalForSecondSet, 15 minutes, 2 days); + require( + withdrawIntervalForSecondSet >= 15 minutes && withdrawIntervalForSecondSet <= 2 days, + "WithdrawInterval is not correctly set" + ); + + // increase time to buy phase of the second round + increaseTime( + buyerAgents[0].createdAt() + swan.getCurrentMarketParameters().sellInterval, + buyerAgents[0], + BuyerAgent.Phase.Buy, + 0 + ); + + // change cycle time + setMarketParameters( + SwanMarketParameters({ + withdrawInterval: withdrawIntervalForFirstSet, + sellInterval: sellIntervalForFirstSet, + buyInterval: buyIntervalForFirstset, + platformFee: 2, + maxAssetCount: 3, + timestamp: block.timestamp, + minAssetPrice: 0.00001 ether, + maxBuyerAgentFee: 80 + }) + ); + + // get all params + SwanMarketParameters[] memory allParams = swan.getMarketParameters(); + assertEq(allParams.length, 2); + (uint256 _currRound, BuyerAgent.Phase _phase,) = buyerAgents[0].getRoundPhase(); + + assertEq(_currRound, 1); + assertEq(uint8(_phase), uint8(BuyerAgent.Phase.Sell)); + + uint256 currTimestamp = block.timestamp; + + // increase time to buy phase of the second round but round comes +1 because of the setMarketParameters call + // buyerAgents[0] should be in buy phase of second round + increaseTime( + currTimestamp + (2 * swan.getCurrentMarketParameters().sellInterval) + + swan.getCurrentMarketParameters().buyInterval + swan.getCurrentMarketParameters().withdrawInterval, + buyerAgents[0], + BuyerAgent.Phase.Buy, + 2 + ); + + // deploy new buyer agent + vm.prank(buyerAgentOwners[0]); + BuyerAgent agentAfterFirstSet = swan.createBuyer( + buyerAgentParameters[1].name, + buyerAgentParameters[1].description, + buyerAgentParameters[1].feeRoyalty, + buyerAgentParameters[1].amountPerRound + ); + + // agentAfterFirstSet should be in sell phase of the first round + checkRoundAndPhase(agentAfterFirstSet, BuyerAgent.Phase.Sell, 0); + + // change cycle time + setMarketParameters( + SwanMarketParameters({ + withdrawInterval: withdrawIntervalForSecondSet, + sellInterval: sellIntervalForSecondSet, + buyInterval: buyIntervalForSecondSet, + platformFee: 2, // percentage + maxAssetCount: 3, + timestamp: block.timestamp, + minAssetPrice: 0.00001 ether, + maxBuyerAgentFee: 80 + }) + ); + + // get all params + allParams = swan.getMarketParameters(); + assertEq(allParams.length, 3); + + // buyerAgents[0] should be in sell phase of the fourth round (2 more increase time + 2 for setting new params) + checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Sell, 3); + + // agentAfterFirstSet should be in sell phase of the second round + checkRoundAndPhase(agentAfterFirstSet, BuyerAgent.Phase.Sell, 1); + } + + function testFuzz_TransferOwnership(address newOwner) public { + vm.assume(newOwner != address(0x0)); + + vm.prank(dria); + swan.transferOwnership(newOwner); + } + + function testFuzz_ListAsset( + string calldata name, + string calldata symbol, + bytes calldata desc, + uint256 price, + string memory agentName, + string memory agentDesc, + uint96 agentFee, + uint256 amountPerRound + ) public fund sellersApproveToSwan { + // Assume the price is within a reasonable range and buyer is not zero address + amountPerRound = bound(amountPerRound, 0.1 ether, 1 ether); + require(amountPerRound >= 0.1 ether && amountPerRound <= 1 ether, "Amount per round is not correctly set"); + + agentFee = uint96(bound(agentFee, 1, marketParameters.maxBuyerAgentFee - 1)); + require(agentFee < marketParameters.maxBuyerAgentFee && agentFee > 0, "Agent fee is not correctly set"); + + price = bound(price, marketParameters.minAssetPrice, amountPerRound - 1); + require(price >= marketParameters.minAssetPrice && price <= amountPerRound - 1, "Price is not correctly set"); + + // Create a buyer agent + vm.prank(buyerAgentOwners[0]); + BuyerAgent _agent = swan.createBuyer(agentName, agentDesc, agentFee, amountPerRound); + + // List the asset + vm.prank(sellers[0]); + swan.list(name, symbol, desc, price, address(_agent)); + + // Check that the asset is listed correctly + address asset = swan.getListedAssets(address(_agent), 0)[0]; + Swan.AssetListing memory listing = swan.getListing(asset); + assertEq(listing.price, price); + assertEq(listing.buyer, address(_agent)); + } +} diff --git a/test/SwanIntervals.t.sol b/test/SwanIntervals.t.sol index 75e0202..dee0532 100644 --- a/test/SwanIntervals.t.sol +++ b/test/SwanIntervals.t.sol @@ -40,99 +40,4 @@ contract SwanIntervalsTest is Helper { ); checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Withdraw, 0); } - - /// @notice Change the intervals and check the current phase and round is are correct - function testFuzz_ChangeCycleTime( - uint256 sellIntervalForFirstSet, - uint256 buyIntervalForFirstset, - uint256 withdrawIntervalForFirstSet, - uint256 sellIntervalForSecondSet, - uint256 buyIntervalForSecondSet, - uint256 withdrawIntervalForSecondSet - ) external createBuyers { - vm.assume(sellIntervalForFirstSet > 15 minutes && sellIntervalForFirstSet < 2 days); - vm.assume(buyIntervalForFirstset > 15 minutes && buyIntervalForFirstset < 2 days); - vm.assume(withdrawIntervalForFirstSet > 15 minutes && withdrawIntervalForFirstSet < 2 days); - vm.assume(sellIntervalForSecondSet > 15 minutes && sellIntervalForSecondSet < 2 days); - vm.assume(buyIntervalForSecondSet > 15 minutes && buyIntervalForSecondSet < 2 days); - vm.assume(withdrawIntervalForSecondSet > 15 minutes && withdrawIntervalForSecondSet < 2 days); - - // increase time to buy phase of the second round - increaseTime( - buyerAgents[0].createdAt() + swan.getCurrentMarketParameters().sellInterval, - buyerAgents[0], - BuyerAgent.Phase.Buy, - 0 - ); - - // change cycle time - setMarketParameters( - SwanMarketParameters({ - withdrawInterval: withdrawIntervalForFirstSet, - sellInterval: sellIntervalForFirstSet, - buyInterval: buyIntervalForFirstset, - platformFee: 2, - maxAssetCount: 3, - timestamp: block.timestamp, - minAssetPrice: 0.00001 ether, - maxBuyerAgentFee: 80 - }) - ); - - // get all params - SwanMarketParameters[] memory allParams = swan.getMarketParameters(); - assertEq(allParams.length, 2); - (uint256 _currRound, BuyerAgent.Phase _phase,) = buyerAgents[0].getRoundPhase(); - - assertEq(_currRound, 1); - assertEq(uint8(_phase), uint8(BuyerAgent.Phase.Sell)); - - uint256 currTimestamp = block.timestamp; - - // increase time to buy phase of the second round but round comes +1 because of the setMarketParameters call - // buyerAgents[0] should be in buy phase of second round - increaseTime( - currTimestamp + (2 * swan.getCurrentMarketParameters().sellInterval) - + swan.getCurrentMarketParameters().buyInterval + swan.getCurrentMarketParameters().withdrawInterval, - buyerAgents[0], - BuyerAgent.Phase.Buy, - 2 - ); - - // deploy new buyer agent - vm.prank(buyerAgentOwners[0]); - BuyerAgent agentAfterFirstSet = swan.createBuyer( - buyerAgentParameters[1].name, - buyerAgentParameters[1].description, - buyerAgentParameters[1].feeRoyalty, - buyerAgentParameters[1].amountPerRound - ); - - // agentAfterFirstSet should be in sell phase of the first round - checkRoundAndPhase(agentAfterFirstSet, BuyerAgent.Phase.Sell, 0); - - // change cycle time - setMarketParameters( - SwanMarketParameters({ - withdrawInterval: withdrawIntervalForSecondSet, - sellInterval: sellIntervalForSecondSet, - buyInterval: buyIntervalForSecondSet, - platformFee: 2, // percentage - maxAssetCount: 3, - timestamp: block.timestamp, - minAssetPrice: 0.00001 ether, - maxBuyerAgentFee: 80 - }) - ); - - // get all params - allParams = swan.getMarketParameters(); - assertEq(allParams.length, 3); - - // buyerAgents[0] should be in sell phase of the fourth round (2 more increase time + 2 for setting new params) - checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Sell, 3); - - // agentAfterFirstSet should be in sell phase of the second round - checkRoundAndPhase(agentAfterFirstSet, BuyerAgent.Phase.Sell, 1); - } }