From 4b04eace6bb536f8e124afb2e8100b1c86b53d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87a=C4=9Fla=20=C3=87elik?= Date: Wed, 20 Nov 2024 16:25:06 +0300 Subject: [PATCH] ref --- .env.example | 13 + .gas-snapshot | 47 ++ .../.github => .github}/workflows/test.yml | 0 .gitignore | 34 + .gitmodules | 13 +- Makefile | 51 ++ README.md | 110 +++ contracts/.gitignore | 14 - contracts/README.md | 66 -- contracts/foundry.toml | 6 - contracts/lib/forge-std | 1 - contracts/libraries/Statistics.sol | 48 ++ contracts/llm/LLMOracleCoordinator.sol | 423 ++++++++++ contracts/llm/LLMOracleManager.sol | 153 ++++ contracts/llm/LLMOracleRegistry.sol | 149 ++++ contracts/llm/LLMOracleTask.sol | 81 ++ contracts/mock/LLMOracleCoordinatorV2.sol | 10 + contracts/mock/LLMOracleRegistryV2.sol | 10 + contracts/mock/SwanV2.sol | 10 + contracts/script/Counter.s.sol | 19 - contracts/src/Counter.sol | 14 - contracts/swan/BuyerAgent.sol | 405 ++++++++++ contracts/swan/Swan.sol | 353 ++++++++ contracts/swan/SwanAsset.sol | 43 + contracts/swan/SwanManager.sol | 140 ++++ contracts/test/Counter.t.sol | 24 - contracts/token/WETH9.sol | 755 ++++++++++++++++++ coverage.sh | 16 + deployment/31337.json | 16 + deployment/84532.json | 16 + foundry.toml | 19 + lcov.info | 673 ++++++++++++++++ script/Deploy.s.sol | 146 ++++ script/HelperConfig.s.sol | 55 ++ storage.sh | 21 + test/BuyerAgent.t.sol | 211 +++++ test/Deploy.t.sol | 40 + test/Helper.t.sol | 396 +++++++++ test/LLMOracleCoordinator.t.sol | 239 ++++++ test/LLMOracleRegistry.t.sol | 156 ++++ test/Swan.t.sol | 568 +++++++++++++ test/SwanIntervals.t.sol | 162 ++++ 42 files changed, 5580 insertions(+), 146 deletions(-) create mode 100644 .env.example create mode 100644 .gas-snapshot rename {contracts/.github => .github}/workflows/test.yml (100%) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md delete mode 100644 contracts/.gitignore delete mode 100644 contracts/README.md delete mode 100644 contracts/foundry.toml delete mode 160000 contracts/lib/forge-std create mode 100644 contracts/libraries/Statistics.sol create mode 100644 contracts/llm/LLMOracleCoordinator.sol create mode 100644 contracts/llm/LLMOracleManager.sol create mode 100644 contracts/llm/LLMOracleRegistry.sol create mode 100644 contracts/llm/LLMOracleTask.sol create mode 100644 contracts/mock/LLMOracleCoordinatorV2.sol create mode 100644 contracts/mock/LLMOracleRegistryV2.sol create mode 100644 contracts/mock/SwanV2.sol delete mode 100644 contracts/script/Counter.s.sol delete mode 100644 contracts/src/Counter.sol create mode 100644 contracts/swan/BuyerAgent.sol create mode 100644 contracts/swan/Swan.sol create mode 100644 contracts/swan/SwanAsset.sol create mode 100644 contracts/swan/SwanManager.sol delete mode 100644 contracts/test/Counter.t.sol create mode 100644 contracts/token/WETH9.sol create mode 100644 coverage.sh create mode 100644 deployment/31337.json create mode 100644 deployment/84532.json create mode 100644 foundry.toml create mode 100644 lcov.info create mode 100644 script/Deploy.s.sol create mode 100644 script/HelperConfig.s.sol create mode 100644 storage.sh create mode 100644 test/BuyerAgent.t.sol create mode 100644 test/Deploy.t.sol create mode 100644 test/Helper.t.sol create mode 100644 test/LLMOracleCoordinator.t.sol create mode 100644 test/LLMOracleRegistry.t.sol create mode 100644 test/Swan.t.sol create mode 100644 test/SwanIntervals.t.sol diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ecb7b43 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Deployer key (REQUIRED) +PUBLIC_KEY= + +### Base URLs ### +# Mainnet +BASE_MAIN_RPC_URL= + +# Testnet +BASE_TEST_RPC_URL=https://sepolia.base.org + +# Blockscout API Key +# Foundry expects the API key to be defined as ETHERSCAN_API_KEY +ETHERSCAN_API_KEY= \ No newline at end of file diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..6ef9dcc --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,47 @@ +BuyerAgentTest:test_InBuyPhase() (gas: 182615337) +BuyerAgentTest:test_InSellPhase() (gas: 182593663) +BuyerAgentTest:test_RevertWhen_SetAmountPerRoundInBuyPhase() (gas: 182596518) +BuyerAgentTest:test_RevertWhen_SetFeeWithInvalidRoyalty() (gas: 182603480) +BuyerAgentTest:test_RevertWhen_SetRoyaltyInSellPhase() (gas: 182595095) +BuyerAgentTest:test_RevertWhen_WithdrawByAnotherOwner() (gas: 182621624) +BuyerAgentTest:test_RevertWhen_WithdrawInBuyPhase() (gas: 182608980) +BuyerAgentTest:test_SetRoyaltyAndAmountPerRound() (gas: 182604536) +BuyerAgentTest:test_WithdrawInWithdrawPhase() (gas: 182585003) +DeployTest:test_Deploy() (gas: 101371) +LLMOracleCoordinatorTest:test_Deployment() (gas: 86478543) +LLMOracleCoordinatorTest:test_RegisterOracles() (gas: 86763309) +LLMOracleCoordinatorTest:test_ValidatorIsGenerator() (gas: 87180676) +LLMOracleCoordinatorTest:test_WithValidation() (gas: 87839545) +LLMOracleCoordinatorTest:test_WithoutValidation() (gas: 87420651) +LLMOracleRegistryTest:test_Deployment() (gas: 18798876) +LLMOracleRegistryTest:test_RegisterGeneratorOracle() (gas: 19137915) +LLMOracleRegistryTest:test_RegisterValidatorOracle() (gas: 19137984) +LLMOracleRegistryTest:test_RevertWhen_RegisterSameGeneratorTwice() (gas: 19141313) +LLMOracleRegistryTest:test_RevertWhen_RegistryHasNotApprovedByOracle() (gas: 18938187) +LLMOracleRegistryTest:test_RevertWhen_UnregisterSameGeneratorTwice() (gas: 19157453) +LLMOracleRegistryTest:test_UnregisterOracle() (gas: 19154341) +LLMOracleRegistryTest:test_WithdrawStakesAfterUnregistering() (gas: 19179317) +SwanIntervalsTest:test_ChangeCycleTime() (gas: 184999199) +SwanIntervalsTest:test_InBuyPhase() (gas: 182573254) +SwanIntervalsTest:test_InSellPhase() (gas: 182568456) +SwanIntervalsTest:test_InWithdrawPhase() (gas: 182576467) +SwanTest:test_CreateBuyerAgents() (gas: 183760248) +SwanTest:test_Deployment() (gas: 179280562) +SwanTest:test_PurchaseAnAsset() (gas: 188748932) +SwanTest:test_RelistAsset() (gas: 187998184) +SwanTest:test_RevertWhen_CreateBuyerWithInvalidRoyalty() (gas: 179349648) +SwanTest:test_RevertWhen_ListInWithdrawPhase() (gas: 183823955) +SwanTest:test_RevertWhen_ListMoreThanMaxAssetCount() (gas: 187892487) +SwanTest:test_RevertWhen_PurchaseByAnotherBuyer() (gas: 187906998) +SwanTest:test_RevertWhen_PurchaseInSellPhase() (gas: 187890335) +SwanTest:test_RevertWhen_PurchaseMoreThanAmountPerRound() (gas: 188775426) +SwanTest:test_RevertWhen_RelistAlreadyPurchasedAsset() (gas: 188743009) +SwanTest:test_RevertWhen_RelistByAnotherSeller() (gas: 187888798) +SwanTest:test_RevertWhen_RelistInBuyPhase() (gas: 187920803) +SwanTest:test_RevertWhen_RelistInTheSameRound() (gas: 187893814) +SwanTest:test_RevertWhen_RelistInWithdrawPhase() (gas: 187920820) +SwanTest:test_SetAmountPerRound() (gas: 183801616) +SwanTest:test_SetFactories() (gas: 183318192) +SwanTest:test_SetMarketParameters() (gas: 183900237) +SwanTest:test_SetOracleParameters() (gas: 183743049) +SwanTest:test_UpdateState() (gas: 189350579) \ No newline at end of file diff --git a/contracts/.github/workflows/test.yml b/.github/workflows/test.yml similarity index 100% rename from contracts/.github/workflows/test.yml rename to .github/workflows/test.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f52211d --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Forge files +cache/ +out/ + +# coverage +coverage/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/*/84532/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + +# Forge files +cache/ +out/ +lib/ + +# gas snapshot +.gas-snapshot/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Dotenv file +.env diff --git a/.gitmodules b/.gitmodules index c65a596..56bfede 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,12 @@ -[submodule "contracts/lib/forge-std"] - path = contracts/lib/forge-std +[submodule "lib/forge-std"] + path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-foundry-upgrades"] + path = lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e0bec1f --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +-include .env + +.PHONY: build test local-key base-sepolia-key deploy anvil install update + +# Capture the network name +network := $(word 2, $(MAKECMDGOALS)) + +# Default to forked base-sepolia network +KEY_NAME := local-key +NETWORK_ARGS := --account local-key --sender 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --broadcast + +ifeq ($(network), base-sepolia) +KEY_NAME := base-sepolia-key +NETWORK_ARGS:= --rpc-url $(BASE_TEST_RPC_URL) --account base-sepolia-key --sender $(PUBLIC_KEY) --broadcast --verify --verifier blockscout --verifier-url https://base-sepolia.blockscout.com/api/ +endif + +# Install Dependencies +install: + forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commit && forge install OpenZeppelin/openzeppelin-foundry-upgrades --no-commit && forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit + +# Build the contracts +build: + forge clean && forge build + +# Generate gas snapshot under snapshots directory +snapshot: + forge snapshot + +# Generate the documentation under docs directory +docs: + forge doc + +# Test the contracts on forked base-sepolia network +test: + forge clean && forge test --fork-url $(BASE_TEST_RPC_URL) + +anvil: + anvil --fork-url $(BASE_TEST_RPC_URL) + +# Create keystores for encrypted private keys by using bls12-381 curve (https://eips.ethereum.org/EIPS/eip-2335) +key: + cast wallet import $(KEY_NAME) --interactive + +# Default to local network if no network is specified +deploy: + forge script ./script/Deploy.s.sol:Deploy $(NETWORK_ARGS) + +# TODO: forge-verify + +# Prevent make from interpreting the network name as a target +$(eval $(network):;@:) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..76da090 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# Swan Protocol + +This document provides instructions for swan contracts using Foundry. + +## Test + +Install dependencies: + +```sh +make install +``` + +Compile the contracts: + +```sh +make build +``` + +Run tests on forked base-sepolia: + +```sh +make test +``` + +## Coverage + +Check coverages with: + +```sh +bash coverage.sh +``` + +You can see coverages under the coverage directory. + +## Storage Layout + +Create storage layout with: + +```sh +bash coverage.sh +``` + +You can see storage layouts under the storage directory. + +## Deployment + +**Step 1.** +Import your `PUBLIC_KEY` and `ETHERSCAN_API_KEY` to env file. + +> [!NOTE] +> +> Foundry expects the API key to be defined as `ETHERSCAN_API_KEY` even though you're using another explorer. + +**Step 2.** +Create keystores for deployment. [See more for keystores](https://eips.ethereum.org/EIPS/eip-2335) + +```sh +make local-key +``` + +or for base-sepolia + +```sh +make base-sepolia-key +``` + +> [!NOTE] +> +> Recommended to create keystores on directly on your shell. +> You HAVE to type your password on the terminal to be able to use your keys. (e.g when deploying a contract) + +**Step 3.** +Enter your private key (associated with the public key you added to env file) and password on terminal. You'll see your public key on terminal. + +> [!NOTE] +> +> If you want to deploy contracts on localhost please provide localhost public key for the command above. + +**Step 4.** Required only for local deployment. + +Start a local node with: + +```sh +make anvil +``` + +**Step 5.** +Deploy the contracts on localhost (forked Base Sepolia) using Deploy script: + +```sh +make deploy +``` + +or Base Sepolia with the command below: + +```sh +make deploy base-sepolia +``` + +You can see deployed contract addresses under the `deployment/.json` + +## Gas Snapshot + +Take the gas snapshot with: + +```sh +make snapshot +``` + +You can see the snapshot `.gas-snapshot` in the current directory. diff --git a/contracts/.gitignore b/contracts/.gitignore deleted file mode 100644 index 85198aa..0000000 --- a/contracts/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Compiler files -cache/ -out/ - -# Ignores development broadcast logs -!/broadcast -/broadcast/*/31337/ -/broadcast/**/dry-run/ - -# Docs -docs/ - -# Dotenv file -.env diff --git a/contracts/README.md b/contracts/README.md deleted file mode 100644 index 9265b45..0000000 --- a/contracts/README.md +++ /dev/null @@ -1,66 +0,0 @@ -## Foundry - -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** - -Foundry consists of: - -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. - -## Documentation - -https://book.getfoundry.sh/ - -## Usage - -### Build - -```shell -$ forge build -``` - -### Test - -```shell -$ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` diff --git a/contracts/foundry.toml b/contracts/foundry.toml deleted file mode 100644 index 25b918f..0000000 --- a/contracts/foundry.toml +++ /dev/null @@ -1,6 +0,0 @@ -[profile.default] -src = "src" -out = "out" -libs = ["lib"] - -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std deleted file mode 160000 index 1eea5ba..0000000 --- a/contracts/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/contracts/libraries/Statistics.sol b/contracts/libraries/Statistics.sol new file mode 100644 index 0000000..8c53643 --- /dev/null +++ b/contracts/libraries/Statistics.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +/// @notice Simple statistic library for uint256 arrays, numbers are treat as fixed-precision floats. +library Statistics { + /// @notice Compute the mean of the data. + /// @param data The data to compute the mean for. + function avg(uint256[] memory data) internal pure returns (uint256 ans) { + uint256 sum = 0; + for (uint256 i = 0; i < data.length; i++) { + sum += data[i]; + } + ans = sum / data.length; + } + + /// @notice Compute the variance of the data. + /// @param data The data to compute the variance for. + function variance(uint256[] memory data) internal pure returns (uint256 ans, uint256 mean) { + mean = avg(data); + uint256 sum = 0; + for (uint256 i = 0; i < data.length; i++) { + uint256 diff = data[i] - mean; + sum += diff * diff; + } + ans = sum / data.length; + } + + /// @notice Compute the standard deviation of the data. + /// @dev Computes variance, and takes the square root. + /// @param data The data to compute the standard deviation for. + function stddev(uint256[] memory data) internal pure returns (uint256 ans, uint256 mean) { + (uint256 _variance, uint256 _mean) = variance(data); + mean = _mean; + ans = sqrt(_variance); + } + + /// @notice Compute the square root of a number. + /// @dev Uses Babylonian method. + /// @param x The number to compute the square root for. + function sqrt(uint256 x) internal pure returns (uint256 y) { + uint256 z = (x + 1) / 2; + y = x; + while (z < y) { + y = z; + z = (x / z + z) / 2; + } + } +} diff --git a/contracts/llm/LLMOracleCoordinator.sol b/contracts/llm/LLMOracleCoordinator.sol new file mode 100644 index 0000000..fd24731 --- /dev/null +++ b/contracts/llm/LLMOracleCoordinator.sol @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {LLMOracleRegistry, LLMOracleKind} from "./LLMOracleRegistry.sol"; +import {LLMOracleTask, LLMOracleTaskParameters} from "./LLMOracleTask.sol"; +import {LLMOracleManager} from "./LLMOracleManager.sol"; +import {Statistics} from "../libraries/Statistics.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +/// @title LLM Oracle Coordinator +/// @notice Responsible for coordinating the Oracle responses to LLM generation requests. +contract LLMOracleCoordinator is LLMOracleTask, LLMOracleManager, UUPSUpgradeable { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Indicates a generation request for LLM. + /// @dev `protocol` is a short 32-byte string (e.g., "dria/1.0.0"). + /// @dev Using the protocol topic, listeners can filter by protocol. + event Request(uint256 indexed taskId, address indexed requester, bytes32 indexed protocol); + + /// @notice Indicates a single Oracle response for a request. + event Response(uint256 indexed taskId, address indexed responder); + + /// @notice Indicates a single Oracle response for a request. + event Validation(uint256 indexed taskId, address indexed validator); + + /// @notice Indicates the status change of an LLM generation request. + event StatusUpdate( + uint256 indexed taskId, bytes32 indexed protocol, TaskStatus statusBefore, TaskStatus statusAfter + ); + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Not enough funds were provided for the task. + error InsufficientFees(uint256 have, uint256 want); + + /// @notice Unexpected status for this task. + error InvalidTaskStatus(uint256 taskId, TaskStatus have, TaskStatus want); + + /// @notice The given nonce is not a valid proof-of-work. + error InvalidNonce(uint256 taskId, uint256 nonce); + + /// @notice The provided validation does not have a score for all responses. + error InvalidValidation(uint256 taskId, address validator); + + /// @notice The oracle is not registered. + error NotRegistered(address oracle); + + /// @notice The oracle has already responded to this task. + error AlreadyResponded(uint256 taskId, address oracle); + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice The Oracle Registry. + LLMOracleRegistry public registry; + /// @notice The token to be used for fee payments. + ERC20 public feeToken; + + /// @notice The task ID counter. + /// @dev TaskId starts from 1, as 0 is reserved. + /// @dev 0 can be used in to check that a request/response/validation has not been made. + uint256 public nextTaskId; + /// @notice LLM generation requests. + mapping(uint256 taskId => TaskRequest) public requests; + /// @notice LLM generation responses. + mapping(uint256 taskId => TaskResponse[]) public responses; + /// @notice LLM generation response validations. + mapping(uint256 taskId => TaskValidation[]) public validations; + + /*////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////*/ + + /// @notice Reverts if `msg.sender` is not a registered oracle. + modifier onlyRegistered(LLMOracleKind kind) { + if (!registry.isRegistered(msg.sender, kind)) { + revert NotRegistered(msg.sender); + } + _; + } + + /// @notice Reverts if the task status is not `status`. + modifier onlyAtStatus(uint256 taskId, TaskStatus status) { + if (requests[taskId].status != status) { + revert InvalidTaskStatus(taskId, requests[taskId].status, status); + } + _; + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Locks the contract, preventing any future re-initialization. + /// @dev [See more](https://docs.openzeppelin.com/contracts/5.x/api/proxy#Initializable-_disableInitializers--). + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /*////////////////////////////////////////////////////////////// + UPGRADABLE + //////////////////////////////////////////////////////////////*/ + + /// @notice Function that should revert when `msg.sender` is not authorized to upgrade the contract. + /// @dev Called by and upgradeToAndCall. + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { + // `onlyOwner` modifier does the auth here + } + + /// @notice Initialize the contract. + /// @notice Sets the Oracle Registry & Oracle Fee Manager. + /// @param _oracleRegistry The Oracle Registry contract address. + /// @param _feeToken The token (ERC20) to be used for fee payments (usually $BATCH). + /// @param _platformFee The initial platform fee for each LLM generation. + /// @param _generationFee The initial base fee for LLM generation. + /// @param _validationFee The initial base fee for response validation. + function initialize( + address _oracleRegistry, + address _feeToken, + uint256 _platformFee, + uint256 _generationFee, + uint256 _validationFee + ) public initializer { + __LLMOracleManager_init(_platformFee, _generationFee, _validationFee); + registry = LLMOracleRegistry(_oracleRegistry); + feeToken = ERC20(_feeToken); + nextTaskId = 1; + } + + /*////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Request LLM generation. + /// @dev Input must be non-empty. + /// @dev Reverts if contract has not enough allowance for the fee. + /// @dev Reverts if difficulty is out of range. + /// @param protocol The protocol string, should be a short 32-byte string (e.g., "dria/1.0.0"). + /// @param input The input data for the LLM generation. + /// @param parameters The task parameters + /// @return task id + function request( + bytes32 protocol, + bytes memory input, + bytes memory models, + LLMOracleTaskParameters calldata parameters + ) public onlyValidParameters(parameters) returns (uint256) { + (uint256 totalfee, uint256 generatorFee, uint256 validatorFee) = getFee(parameters); + + // check allowance requirements + uint256 allowance = feeToken.allowance(msg.sender, address(this)); + if (allowance < totalfee) { + revert InsufficientFees(allowance, totalfee); + } + + // ensure there is enough balance + uint256 balance = feeToken.balanceOf(msg.sender); + if (balance < totalfee) { + revert InsufficientFees(balance, totalfee); + } + + // transfer tokens + feeToken.transferFrom(msg.sender, address(this), totalfee); + + // increment the task id for later tasks & emit task request event + uint256 taskId = nextTaskId; + unchecked { + ++nextTaskId; + } + emit Request(taskId, msg.sender, protocol); + + // push request & emit status update for the task + requests[taskId] = TaskRequest({ + requester: msg.sender, + protocol: protocol, + input: input, + parameters: parameters, + status: TaskStatus.PendingGeneration, + generatorFee: generatorFee, + validatorFee: validatorFee, + platformFee: platformFee, + models: models + }); + emit StatusUpdate(taskId, protocol, TaskStatus.None, TaskStatus.PendingGeneration); + + return taskId; + } + + /// @notice Respond to an LLM generation. + /// @dev Output must be non-empty. + /// @dev Reverts if the task is not pending generation. + /// @dev Reverts if the responder is not registered. + /// @dev Reverts if the responder has already responded to this task. + /// @dev Reverts if the nonce is not a valid proof-of-work. + /// @param taskId The task ID to respond to. + /// @param nonce The proof-of-work nonce. + /// @param output The output data for the LLM generation. + /// @param metadata Optional metadata for this output. + function respond(uint256 taskId, uint256 nonce, bytes calldata output, bytes calldata metadata) + public + onlyRegistered(LLMOracleKind.Generator) + onlyAtStatus(taskId, TaskStatus.PendingGeneration) + { + TaskRequest storage task = requests[taskId]; + + // ensure responder to be unique for this task + for (uint256 i = 0; i < responses[taskId].length; i++) { + if (responses[taskId][i].responder == msg.sender) { + revert AlreadyResponded(taskId, msg.sender); + } + } + + // check nonce (proof-of-work) + assertValidNonce(taskId, task, nonce); + + // push response + TaskResponse memory response = + TaskResponse({responder: msg.sender, nonce: nonce, output: output, metadata: metadata, score: 0}); + responses[taskId].push(response); + + // emit response events + emit Response(taskId, msg.sender); + + // send rewards to the generator if there is no validation + if (task.parameters.numValidations == 0) { + _increaseAllowance(msg.sender, task.generatorFee); + } + + // check if we have received enough responses & update task status + bool isCompleted = responses[taskId].length == uint256(task.parameters.numGenerations); + if (isCompleted) { + if (task.parameters.numValidations == 0) { + // no validations required, task is completed + task.status = TaskStatus.Completed; + emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingGeneration, TaskStatus.Completed); + } else { + // now we are waiting for validations + task.status = TaskStatus.PendingValidation; + emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingGeneration, TaskStatus.PendingValidation); + } + } + } + + /// @notice Validate requests for a given taskId. + /// @dev Reverts if the task is not pending validation. + /// @dev Reverts if the number of scores is not equal to the number of generations. + /// @dev Reverts if any score is greater than the maximum score. + /// @param taskId The ID of the task to validate. + /// @param nonce The proof-of-work nonce. + /// @param scores The validation scores for each generation. + /// @param metadata Optional metadata for this validation. + function validate(uint256 taskId, uint256 nonce, uint256[] calldata scores, bytes calldata metadata) + public + onlyRegistered(LLMOracleKind.Validator) + onlyAtStatus(taskId, TaskStatus.PendingValidation) + { + TaskRequest storage task = requests[taskId]; + + // ensure there is a score for each generation + if (scores.length != task.parameters.numGenerations) { + revert InvalidValidation(taskId, msg.sender); + } + + // ensure validator did not participate in generation + for (uint256 i = 0; i < task.parameters.numGenerations; i++) { + if (responses[taskId][i].responder == msg.sender) { + revert AlreadyResponded(taskId, msg.sender); + } + } + + // ensure validator to be unique for this task + for (uint256 i = 0; i < validations[taskId].length; i++) { + if (validations[taskId][i].validator == msg.sender) { + revert AlreadyResponded(taskId, msg.sender); + } + } + + // check nonce (proof-of-work) + assertValidNonce(taskId, task, nonce); + + // update validation scores + validations[taskId].push( + TaskValidation({scores: scores, nonce: nonce, metadata: metadata, validator: msg.sender}) + ); + + // emit validation event + emit Validation(taskId, msg.sender); + + // update completion status + bool isCompleted = validations[taskId].length == task.parameters.numValidations; + if (isCompleted) { + task.status = TaskStatus.Completed; + emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingValidation, TaskStatus.Completed); + + // finalize validation scores + finalizeValidation(taskId); + } + } + + /// @notice Checks that proof-of-work is valid for a given task with taskId and nonce. + /// @dev Reverts if the nonce is not a valid proof-of-work. + /// @param taskId The ID of the task to check proof-of-work. + /// @param task The task (in storage) to validate. + /// @param nonce The candidate proof-of-work nonce. + function assertValidNonce(uint256 taskId, TaskRequest storage task, uint256 nonce) internal view { + bytes memory message = abi.encodePacked(taskId, task.input, task.requester, msg.sender, nonce); + if (uint256(keccak256(message)) > type(uint256).max >> uint256(task.parameters.difficulty)) { + revert InvalidNonce(taskId, nonce); + } + } + + /// @notice Compute the validation scores for a given task. + /// @dev Reverts if the task has no validations. + /// @param taskId The ID of the task to compute scores for. + function finalizeValidation(uint256 taskId) private { + TaskRequest storage task = requests[taskId]; + + // compute score for each generation + for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) { + // get the scores for this generation, i.e. the g_i-th element of each validation + uint256[] memory scores = new uint256[](task.parameters.numValidations); + for (uint256 v_i = 0; v_i < task.parameters.numValidations; v_i++) { + scores[v_i] = validations[taskId][v_i].scores[g_i]; + } + + // compute the mean and standard deviation + (uint256 _stddev, uint256 _mean) = Statistics.stddev(scores); + + // compute the score for this generation as the "inner-mean" + // and send rewards to validators that are within the range + uint256 innerSum = 0; + uint256 innerCount = 0; + for (uint256 v_i = 0; v_i < task.parameters.numValidations; ++v_i) { + uint256 score = scores[v_i]; + if ((score >= _mean - _stddev) && (score <= _mean + _stddev)) { + innerSum += score; + innerCount++; + + // send validation fee to the validator + _increaseAllowance(validations[taskId][v_i].validator, task.validatorFee); + } + } + + // set score for this generation as the average of inner scores + uint256 inner_score = innerCount == 0 ? 0 : innerSum / innerCount; + responses[taskId][g_i].score = inner_score; + } + + // now, we have the scores for each generation + // compute stddev for these and pick the ones above a threshold + uint256[] memory generationScores = new uint256[](task.parameters.numGenerations); + for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) { + generationScores[g_i] = responses[taskId][g_i].score; + } + + // compute the mean and standard deviation + (uint256 stddev, uint256 mean) = Statistics.stddev(generationScores); + for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) { + // ignore lower outliers + if (generationScores[g_i] >= mean - generationDeviationFactor * stddev) { + _increaseAllowance(responses[taskId][g_i].responder, task.generatorFee); + } + } + } + + /// @notice Withdraw the platform fees & along with remaining fees within the contract. + function withdrawPlatformFees() public onlyOwner { + feeToken.transfer(owner(), feeToken.balanceOf(address(this))); + } + + /// @notice Returns the responses to a given taskId. + /// @param taskId The ID of the task to get responses for. + /// @return The responses for the given taskId. + function getResponses(uint256 taskId) public view returns (TaskResponse[] memory) { + return responses[taskId]; + } + + /// @notice Returns the validations to a given taskId. + /// @param taskId The ID of the task to get validations for. + /// @return The validations for the given taskId. + function getValidations(uint256 taskId) public view returns (TaskValidation[] memory) { + return validations[taskId]; + } + + /// Increases the allowance by setting the approval to the sum of the current allowance and the additional amount. + /// @param spender spender address + /// @param amount additional amount of allowance + function _increaseAllowance(address spender, uint256 amount) internal { + feeToken.approve(spender, feeToken.allowance(address(this), spender) + amount); + } + + /// @notice Returns the best performing result of the given task. + /// @dev For invalid task IDs, the status check will fail. + /// @param taskId The ID of the task to get the result for. + /// @return The best performing response w.r.t validation scores. + function getBestResponse(uint256 taskId) external view returns (TaskResponse memory) { + TaskResponse[] storage taskResponses = responses[taskId]; + + // ensure that task is completed + if (requests[taskId].status != LLMOracleTask.TaskStatus.Completed) { + revert InvalidTaskStatus(taskId, requests[taskId].status, LLMOracleTask.TaskStatus.Completed); + } + + // pick the result with the highest validation score + TaskResponse storage result = taskResponses[0]; + uint256 highestScore = result.score; + for (uint256 i = 1; i < taskResponses.length; i++) { + if (taskResponses[i].score > highestScore) { + highestScore = taskResponses[i].score; + result = taskResponses[i]; + } + } + + return result; + } +} diff --git a/contracts/llm/LLMOracleManager.sol b/contracts/llm/LLMOracleManager.sol new file mode 100644 index 0000000..bfbdcf8 --- /dev/null +++ b/contracts/llm/LLMOracleManager.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {LLMOracleTaskParameters} from "./LLMOracleTask.sol"; + +/// @title LLM Oracle Manager +/// @notice Holds the configuration for the LLM Oracle, such as allowed bounds on difficulty, +/// number of generations & validations, and fee settings. + +contract LLMOracleManager is OwnableUpgradeable { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Given parameter is out of range. + error InvalidParameterRange(uint256 have, uint256 min, uint256 max); + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice A fixed fee paid for the platform. + uint256 public platformFee; + /// @notice The base fee factor for a generation of LLM generation. + /// @dev When scaled with difficulty & number of generations, we denote it as `generatorFee`. + uint256 public generationFee; + /// @notice The base fee factor for a generation of LLM validation. + /// @dev When scaled with difficulty & number of validations, we denote it as `validatorFee`. + uint256 public validationFee; + + /// @notice The deviation factor for the validation scores. + uint64 public validationDeviationFactor; + /// @notice The deviation factor for the generation scores. + uint64 public generationDeviationFactor; + + /// @notice Minimums for oracle parameters. + LLMOracleTaskParameters minimumParameters; + /// @notice Maximums for oracle parameters. + LLMOracleTaskParameters maximumParameters; + + /*////////////////////////////////////////////////////////////// + UPGRADABLE + //////////////////////////////////////////////////////////////*/ + + /// @notice Initialize the contract. + function __LLMOracleManager_init(uint256 _platformFee, uint256 _generationFee, uint256 _validationFee) + internal + onlyInitializing + { + __Ownable_init(msg.sender); + __LLMOracleManager_init_unchained(_platformFee, _generationFee, _validationFee); + } + + function __LLMOracleManager_init_unchained(uint256 _platformFee, uint256 _generationFee, uint256 _validationFee) + internal + onlyInitializing + { + minimumParameters = LLMOracleTaskParameters({difficulty: 1, numGenerations: 1, numValidations: 0}); + maximumParameters = LLMOracleTaskParameters({difficulty: 10, numGenerations: 10, numValidations: 10}); + + validationDeviationFactor = 2; + generationDeviationFactor = 1; + + setFees(_platformFee, _generationFee, _validationFee); + } + + /*////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Modifier to check if the given parameters are within the allowed range. + modifier onlyValidParameters(LLMOracleTaskParameters calldata parameters) { + if ( + parameters.difficulty < minimumParameters.difficulty || parameters.difficulty > maximumParameters.difficulty + ) { + revert InvalidParameterRange( + parameters.difficulty, minimumParameters.difficulty, maximumParameters.difficulty + ); + } + + if ( + parameters.numGenerations < minimumParameters.numGenerations + || parameters.numGenerations > maximumParameters.numGenerations + ) { + revert InvalidParameterRange( + parameters.numGenerations, minimumParameters.numGenerations, maximumParameters.numGenerations + ); + } + + if ( + parameters.numValidations < minimumParameters.numValidations + || parameters.numValidations > maximumParameters.numValidations + ) { + revert InvalidParameterRange( + parameters.numValidations, minimumParameters.numValidations, maximumParameters.numValidations + ); + } + _; + } + + /// @notice Update Oracle fees. + /// @dev To keep a fee unchanged, provide the same value. + /// @param _platformFee The new platform fee + /// @param _generationFee The new generation fee + /// @param _validationFee The new validation fee + function setFees(uint256 _platformFee, uint256 _generationFee, uint256 _validationFee) public onlyOwner { + platformFee = _platformFee; + generationFee = _generationFee; + validationFee = _validationFee; + } + + /// @notice Get the total fee for a given task setting. + /// @param parameters The task parameters. + /// @return totalFee The total fee for the task. + /// @return generatorFee The fee paid to each generator per generation. + /// @return validatorFee The fee paid to each validator per validated generation. + function getFee(LLMOracleTaskParameters calldata parameters) + public + view + returns (uint256 totalFee, uint256 generatorFee, uint256 validatorFee) + { + uint256 diff = (2 << uint256(parameters.difficulty)); + generatorFee = diff * generationFee; + validatorFee = diff * validationFee; + totalFee = + platformFee + (parameters.numGenerations * (generatorFee + (parameters.numValidations * validatorFee))); + } + + /// @notice Update Oracle parameters bounds. + /// @dev Provide the same value to keep it unchanged. + /// @param minimums The new minimum parameters. + /// @param maximums The new maximum parameters. + function setParameters(LLMOracleTaskParameters calldata minimums, LLMOracleTaskParameters calldata maximums) + public + onlyOwner + { + minimumParameters = minimums; + maximumParameters = maximums; + } + + /// @notice Update deviation factors. + /// @dev Provide the same value to keep it unchanged. + /// @param _generationDeviationFactor The new generation deviation factor. + /// @param _validationDeviationFactor The new validation deviation factor. + function setDeviationFactors(uint64 _generationDeviationFactor, uint64 _validationDeviationFactor) + public + onlyOwner + { + generationDeviationFactor = _generationDeviationFactor; + validationDeviationFactor = _validationDeviationFactor; + } +} diff --git a/contracts/llm/LLMOracleRegistry.sol b/contracts/llm/LLMOracleRegistry.sol new file mode 100644 index 0000000..a585d06 --- /dev/null +++ b/contracts/llm/LLMOracleRegistry.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +/// @notice The type of Oracle. +enum LLMOracleKind { + Generator, + Validator +} + +/// @title LLM Oracle Registry +/// @notice Holds the addresses that are eligible to respond to LLM requests. +/// @dev There may be several types of oracle kinds, and each require their own stake. +contract LLMOracleRegistry is OwnableUpgradeable, UUPSUpgradeable { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice The Oracle response to an LLM generation request. + event Registered(address indexed, LLMOracleKind kind); + + /// @notice The Oracle response to an LLM generation request. + event Unregistered(address indexed, LLMOracleKind kind); + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice The user is not registered. + error NotRegistered(address); + + /// @notice The user is already registered. + error AlreadyRegistered(address); + + /// @notice Insufficient stake amount during registration. + error InsufficientFunds(); + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice Stake amount to be registered as an Oracle that can serve generation requests. + uint256 public generatorStakeAmount; + + /// @notice Stake amount to be registered as an Oracle that can serve validation requests. + uint256 public validatorStakeAmount; + + /// @notice Registrations per address & kind. If amount is 0, it is not registered. + mapping(address oracle => mapping(LLMOracleKind => uint256 amount)) public registrations; + + /// @notice Token used for staking. + ERC20 public token; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Locks the contract, preventing any future re-initialization. + /// @dev [See more](https://docs.openzeppelin.com/contracts/5.x/api/proxy#Initializable-_disableInitializers--). + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /*////////////////////////////////////////////////////////////// + UPGRADABLE + //////////////////////////////////////////////////////////////*/ + + /// @notice Function that should revert when `msg.sender` is not authorized to upgrade the contract. + /// @dev Called by and upgradeToAndCall. + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + /// @dev Sets the owner to be the deployer, sets initial stake amount. + function initialize(uint256 _generatorStakeAmount, uint256 _validatorStakeAmount, address _token) + public + initializer + { + __Ownable_init(msg.sender); + generatorStakeAmount = _generatorStakeAmount; + validatorStakeAmount = _validatorStakeAmount; + token = ERC20(_token); + } + + /*////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Register an Oracle. + /// @dev Reverts if the user is already registered or has insufficient funds. + /// @param kind The kind of Oracle to unregister. + function register(LLMOracleKind kind) public { + uint256 amount = getStakeAmount(kind); + + // ensure the user is not already registered + if (isRegistered(msg.sender, kind)) { + revert AlreadyRegistered(msg.sender); + } + + // ensure the user has enough allowance to stake + if (token.allowance(msg.sender, address(this)) < amount) { + revert InsufficientFunds(); + } + token.transferFrom(msg.sender, address(this), amount); + + // register the user + registrations[msg.sender][kind] = amount; + emit Registered(msg.sender, kind); + } + + /// @notice Remove registration of an Oracle. + /// @dev Reverts if the user is not registered. + /// @param kind The kind of Oracle to unregister. + /// @return amount Amount of stake approved back. + function unregister(LLMOracleKind kind) public returns (uint256 amount) { + amount = registrations[msg.sender][kind]; + + // ensure the user is registered + if (amount == 0) { + revert NotRegistered(msg.sender); + } + + // unregister the user + delete registrations[msg.sender][kind]; + emit Unregistered(msg.sender, kind); + + // approve its stake back + token.approve(msg.sender, token.allowance(address(this), msg.sender) + amount); + } + + /// @notice Set the stake amount required to register as an Oracle. + /// @dev Only allowed by the owner. + function setStakeAmounts(uint256 _generatorStakeAmount, uint256 _validatorStakeAmount) public onlyOwner { + generatorStakeAmount = _generatorStakeAmount; + validatorStakeAmount = _validatorStakeAmount; + } + + /// @notice Returns the stake amount required to register as an Oracle w.r.t given kind. + function getStakeAmount(LLMOracleKind kind) public view returns (uint256) { + return kind == LLMOracleKind.Generator ? generatorStakeAmount : validatorStakeAmount; + } + + /// @notice Check if an Oracle is registered. + function isRegistered(address user, LLMOracleKind kind) public view returns (bool) { + return registrations[user][kind] != 0; + } +} diff --git a/contracts/llm/LLMOracleTask.sol b/contracts/llm/LLMOracleTask.sol new file mode 100644 index 0000000..3519429 --- /dev/null +++ b/contracts/llm/LLMOracleTask.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +/// @notice Collection of oracle task-related parameters. +/// @dev Prevents stack-too-deep with tight-packing. +/// TODO: use 256-bit tight-packing here +struct LLMOracleTaskParameters { + /// @notice Difficulty of the task. + uint8 difficulty; + /// @notice Number of generations. + uint40 numGenerations; + /// @notice Number of validations. + uint40 numValidations; +} + +/// @title LLM Oracle Task Interface +/// @notice An umbrella interface that captures task-related structs and enums. +interface LLMOracleTask { + /// @notice Task status. + /// @dev `None`: Task has not been created yet. (default) + /// @dev `PendingGeneration`: Task is waiting for Oracle generation responses. + /// @dev `PendingValidation`: Task is waiting for validation by validator Oracles. + /// @dev `Completed`: The task has been completed. + /// @dev With validation, the flow is `None -> PendingGeneration -> PendingValidation -> Completed`. + /// @dev Without validation, the flow is `None -> PendingGeneration -> Completed`. + enum TaskStatus { + None, + PendingGeneration, + PendingValidation, + Completed + } + + /// @notice A task request for LLM generation. + /// @dev Fees are stored here as well in case fee changes occur within the duration of a task. + struct TaskRequest { + /// @dev Requesting address, also responsible of the fee payment. + address requester; + /// @dev Protocol string, such as `dria/0.1.0`. + bytes32 protocol; + /// @dev Task parameters, e.g. difficulty and number of generations & validations. + LLMOracleTaskParameters parameters; + /// @dev Task status. + TaskStatus status; + /// @dev Fee paid to each generator per generation. + uint256 generatorFee; + /// @dev Fee paid to each validator per validated generation. + uint256 validatorFee; + /// @dev Fee paid to the platform + uint256 platformFee; + /// @dev Input data for the task, usually a human-readable string. + bytes input; + /// @dev Allowed model names for the task. + bytes models; + } + + /// @notice A task response to an LLM generation request. + struct TaskResponse { + /// @dev Responding Oracle address. + address responder; + /// @dev Proof-of-Work nonce for SHA3(taskId, input, requester, responder, nonce) < difficulty. + uint256 nonce; + /// @dev Final validation score assigned by validators, stays 0 if there is no validation. + uint256 score; + /// @dev Output data for the task, usually the direct output of LLM. + bytes output; + /// @dev Optional metadata for this generation. + bytes metadata; + } + + /// @notice A task validation for a response. + struct TaskValidation { + /// @dev Responding validator address. + address validator; + /// @dev Proof-of-Work nonce for SHA3(taskId, input, requester, responder, nonce) < difficulty. + uint256 nonce; + /// @dev Validation scores + uint256[] scores; + /// @dev Optional metadata for this validation. + bytes metadata; + } +} diff --git a/contracts/mock/LLMOracleCoordinatorV2.sol b/contracts/mock/LLMOracleCoordinatorV2.sol new file mode 100644 index 0000000..8060a68 --- /dev/null +++ b/contracts/mock/LLMOracleCoordinatorV2.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {LLMOracleCoordinator} from "../llm/LLMOracleCoordinator.sol"; + +contract LLMOracleCoordinatorV2 is LLMOracleCoordinator { + function upgraded() public view virtual returns (bool) { + return true; + } +} diff --git a/contracts/mock/LLMOracleRegistryV2.sol b/contracts/mock/LLMOracleRegistryV2.sol new file mode 100644 index 0000000..dcba1b6 --- /dev/null +++ b/contracts/mock/LLMOracleRegistryV2.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {LLMOracleRegistry} from "../llm/LLMOracleRegistry.sol"; + +contract LLMOracleRegistryV2 is LLMOracleRegistry { + function upgraded() public view virtual returns (bool) { + return true; + } +} diff --git a/contracts/mock/SwanV2.sol b/contracts/mock/SwanV2.sol new file mode 100644 index 0000000..a75dfe8 --- /dev/null +++ b/contracts/mock/SwanV2.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Swan} from "../swan/Swan.sol"; + +contract SwanV2 is Swan { + function upgraded() public view virtual returns (bool) { + return true; + } +} diff --git a/contracts/script/Counter.s.sol b/contracts/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/contracts/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/contracts/src/Counter.sol b/contracts/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/contracts/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/contracts/swan/BuyerAgent.sol b/contracts/swan/BuyerAgent.sol new file mode 100644 index 0000000..fc70cd4 --- /dev/null +++ b/contracts/swan/BuyerAgent.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {LLMOracleTask, LLMOracleTaskParameters} from "../llm/LLMOracleTask.sol"; +import {Swan, SwanBuyerPurchaseOracleProtocol, SwanBuyerStateOracleProtocol} from "../swan/Swan.sol"; +import {SwanMarketParameters} from "../swan/SwanManager.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +/// @notice Factory contract to deploy BuyerAgent contracts. +/// @dev This saves from contract space for Swan. +contract BuyerAgentFactory { + function deploy( + string memory _name, + string memory _description, + uint96 _royaltyFee, + uint256 _amountPerRound, + address _owner + ) external returns (BuyerAgent) { + return new BuyerAgent(_name, _description, _royaltyFee, _amountPerRound, msg.sender, _owner); + } +} + +/// @notice BuyerAgent is responsible for buying the assets from Swan. +contract BuyerAgent is Ownable, IERC721Receiver { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice The `value` is less than `minFundAmount` + error MinFundSubceeded(uint256 value); + + /// @notice Given fee is invalid, e.g. not within the range. + error InvalidFee(uint256 fee); + + /// @notice Asset count limit exceeded for this round + error BuyLimitExceeded(uint256 have, uint256 want); + + /// @notice Invalid phase + error InvalidPhase(Phase have, Phase want); + + /// @notice Unauthorized caller. + error Unauthorized(address caller); + + /// @notice No task request has been made yet. + error TaskNotRequested(); + + /// @notice The task was already processed, via `purchase` or `updateState`. + error TaskAlreadyProcessed(); + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice Phase of the purchase loop. + enum Phase { + Sell, + Buy, + Withdraw + } + + /// @notice Swan contract. + Swan public immutable swan; + /// @notice Timestamp when the contract is deployed. + uint256 public immutable createdAt; + + /// @notice Holds the index of the Swan market parameters at the time of deployment. + /// @dev When calculating the round, we will use this index to determine the start interval. + uint256 public immutable marketParameterIdx; + + /// @notice Buyer agent name. + string public name; + /// @notice Buyer agent description, can include backstory, behavior and objective together. + string public description; + /// @notice State of the buyer agent. + /// @dev Only updated by the oracle via `updateState`. + bytes public state; + /// @notice Royalty fees for the buyer agent. + uint96 public royaltyFee; + /// @notice The max amount of money the agent can spend per round. + uint256 public amountPerRound; + + /// @notice The assets that the buyer agent has. + mapping(uint256 round => address[] assets) public inventory; + /// @notice Amount of money spent on each round. + mapping(uint256 round => uint256 spending) public spendings; + + /// @notice Oracle requests for each round about item purchases. + /// @dev A taskId of 0 means no request has been made. + mapping(uint256 round => uint256 taskId) public oraclePurchaseRequests; + /// @notice Oracle requests for each round about buyer state updates. + /// @dev A taskId of 0 means no request has been made. + /// @dev A non-zero taskId means a request has been made, but not necessarily processed. + /// @dev To see if a task is completed, check `isOracleTaskProcessed`. + mapping(uint256 round => uint256 taskId) public oracleStateRequests; + /// @notice Indicates whether a given task has been processed. + /// @dev This is used to prevent double processing of the same task. + mapping(uint256 taskId => bool isProcessed) public isOracleRequestProcessed; + + /*////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////*/ + + /// @notice Check if the caller is the owner, operator, or Swan. + /// @dev Swan is an operator itself, so the first check handles that as well. + modifier onlyAuthorized() { + // if its not an operator, and is not an owner, it is unauthorized + if (!swan.isOperator(msg.sender) && msg.sender != owner()) { + revert Unauthorized(msg.sender); + } + _; + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Create the buyer agent. + /// @dev `_royaltyFee` should be between 1 and 100. + /// @dev All tokens are approved to the oracle coordinator of operator. + constructor( + string memory _name, + string memory _description, + uint96 _royaltyFee, + uint256 _amountPerRound, + address _operator, + address _owner + ) Ownable(_owner) { + if (_royaltyFee < 1 || _royaltyFee > 100) { + revert InvalidFee(_royaltyFee); + } + royaltyFee = _royaltyFee; + + swan = Swan(_operator); + amountPerRound = _amountPerRound; + name = _name; + description = _description; + createdAt = block.timestamp; + marketParameterIdx = swan.getMarketParameters().length - 1; + + // approve the coordinator to take fees + // a max approval results in infinite allowance + swan.token().approve(address(swan.coordinator()), type(uint256).max); + swan.token().approve(address(swan), type(uint256).max); + } + + /// @notice Function to receive ERC721 tokens via safe transfer. + /// @dev [See more](https://eips.ethereum.org/EIPS/eip-721). + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return this.onERC721Received.selector; + } + + /*////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice The minimum amount of money that the buyer must leave within the contract. + /// @dev minFundAmount = amountPerRound + 2 * oracleTotalFee + function minFundAmount() public view returns (uint256) { + // 2 * oracleFee => one for the next round and one for update state + return amountPerRound + 2 * swan.getOracleFee(); + } + + /// @notice Reads the best performing result for a given task id, and parses it as an array of addresses. + /// @param taskId task id to be read + function oracleResult(uint256 taskId) public view returns (bytes memory) { + // task id must be non-zero + if (taskId == 0) { + revert TaskNotRequested(); + } + + return swan.coordinator().getBestResponse(taskId).output; + } + + /// @notice Calls the LLMOracleCoordinator & pays for the oracle fees to make a state update request. + /// @param _input input to the LLMOracleCoordinator. + /// @param _models models to be used for the oracle. + /// @dev Works only in `Withdraw` phase. + /// @dev Calling again in the same round will overwrite the previous request. + /// The operator must check that there is no request in beforehand, + /// so to not overwrite an existing request of the owner. + function oracleStateRequest(bytes calldata _input, bytes calldata _models) external onlyAuthorized { + // check that we are in the Withdraw phase, and return round + (uint256 round,) = _checkRoundPhase(Phase.Withdraw); + + oracleStateRequests[round] = + swan.coordinator().request(SwanBuyerStateOracleProtocol, _input, _models, swan.getOracleParameters()); + } + + /// @notice Calls the LLMOracleCoordinator & pays for the oracle fees to make a purchase request. + /// @param _input input to the LLMOracleCoordinator. + /// @param _models models to be used for the oracle. + /// @dev Works only in `Buy` phase. + /// @dev Calling again in the same round will overwrite the previous request. + /// The operator must check that there is no request in beforehand, + /// so to not overwrite an existing request of the owner. + function oraclePurchaseRequest(bytes calldata _input, bytes calldata _models) external onlyAuthorized { + // check that we are in the Buy phase, and return round + (uint256 round,) = _checkRoundPhase(Phase.Buy); + + oraclePurchaseRequests[round] = + swan.coordinator().request(SwanBuyerPurchaseOracleProtocol, _input, _models, swan.getOracleParameters()); + } + + /// @notice Function to update the Buyer state. + /// @dev Works only in `Withdraw` phase. + /// @dev Can be called multiple times within a single round, although is not expected to be done so. + function updateState() external onlyAuthorized { + // check that we are in the Withdraw phase, and return round + (uint256 round,) = _checkRoundPhase(Phase.Withdraw); + + // check if the task is already processed + uint256 taskId = oracleStateRequests[round]; + if (isOracleRequestProcessed[taskId]) { + revert TaskAlreadyProcessed(); + } + + // read oracle result using the task id for this round + bytes memory newState = oracleResult(taskId); + state = newState; + + // update taskId as completed + isOracleRequestProcessed[taskId] = true; + } + + /// @notice Function to buy the asset from the Swan with the given assed address. + /// @dev Works only in `Buy` phase. + /// @dev Can be called multiple times within a single round, although is not expected to be done so. + /// @dev This is not expected to revert if the oracle works correctly. + function purchase() external onlyAuthorized { + // check that we are in the Buy phase, and return round + (uint256 round,) = _checkRoundPhase(Phase.Buy); + + // check if the task is already processed + uint256 taskId = oraclePurchaseRequests[round]; + if (isOracleRequestProcessed[taskId]) { + revert TaskAlreadyProcessed(); + } + + // read oracle result using the latest task id for this round + bytes memory output = oracleResult(taskId); + address[] memory assets = abi.decode(output, (address[])); + + // we purchase each asset returned + for (uint256 i = 0; i < assets.length; i++) { + address asset = assets[i]; + + // must not exceed the roundly buy-limit + uint256 price = swan.getListingPrice(asset); + spendings[round] += price; + if (spendings[round] > amountPerRound) { + revert BuyLimitExceeded(spendings[round], amountPerRound); + } + + // add to inventory + inventory[round].push(asset); + + // make the actual purchase + swan.purchase(asset); + } + + // update taskId as completed + isOracleRequestProcessed[taskId] = true; + } + + /// @notice Function to withdraw the tokens from the contract. + /// @param _amount amount to withdraw. + /// @dev If the current phase is `Withdraw` buyer can withdraw any amount of tokens. + /// @dev If the current phase is not `Withdraw` buyer has to leave at least `minFundAmount` in the contract. + function withdraw(uint96 _amount) public onlyAuthorized { + (, Phase phase,) = getRoundPhase(); + + // if we are not in Withdraw phase, we must leave + // at least minFundAmount in the contract + if (phase != Phase.Withdraw) { + // instead of checking `treasury - _amount < minFoundAmount` + // we check this way to prevent underflows + if (treasury() < minFundAmount() + _amount) { + revert MinFundSubceeded(_amount); + } + } + + // transfer the tokens to the owner of Buyer + swan.token().transfer(owner(), _amount); + } + + /// @notice Alias to get the token balance of buyer agent. + /// @return token balance + function treasury() public view returns (uint256) { + return swan.token().balanceOf(address(this)); + } + + /// @notice Checks that we are in the given phase, and returns both round and phase. + /// @param _phase expected phase. + function _checkRoundPhase(Phase _phase) internal view returns (uint256, Phase) { + (uint256 round, Phase phase,) = getRoundPhase(); + if (phase != _phase) { + revert InvalidPhase(phase, _phase); + } + + return (round, phase); + } + + /// @notice Computes cycle time by using intervals from given market parameters. + /// @dev Used in 'computePhase()' function. + /// @param params Market parameters of the Swan. + /// @return the total cycle time that is `sellInterval + buyInterval + withdrawInterval`. + function _computeCycleTime(SwanMarketParameters memory params) internal pure returns (uint256) { + return params.sellInterval + params.buyInterval + params.withdrawInterval; + } + + /// @notice Function to compute the current round, phase and time until next phase w.r.t given market parameters. + /// @param params Market parameters of the Swan. + /// @param elapsedTime Time elapsed that computed in 'getRoundPhase()' according to the timestamps of each round. + /// @return round, phase, time until next phase + function _computePhase(SwanMarketParameters memory params, uint256 elapsedTime) + internal + pure + returns (uint256, Phase, uint256) + { + uint256 cycleTime = _computeCycleTime(params); + uint256 round = elapsedTime / cycleTime; + uint256 roundTime = elapsedTime % cycleTime; + + // example: + // |-------------> | (roundTime) + // |--Sell--|--Buy--|-Withdraw-| (cycleTime) + if (roundTime < params.sellInterval) { + return (round, Phase.Sell, params.sellInterval - roundTime); + } else if (roundTime < params.sellInterval + params.buyInterval) { + return (round, Phase.Buy, params.sellInterval + params.buyInterval - roundTime); + } else { + return (round, Phase.Withdraw, cycleTime - roundTime); + } + } + + /// @notice Function to return the current round, elapsed round and the current phase according to the current time. + /// @dev Each round is composed of three phases in order: Sell, Buy, Withdraw. + /// @dev Internally, it computes the intervals from market parameters at the creation of this agent, until now. + /// @dev If there are many parameter changes throughout the life of this agent, this may cost more GAS. + /// @return round, phase, time until next phase + function getRoundPhase() public view returns (uint256, Phase, uint256) { + SwanMarketParameters[] memory marketParams = swan.getMarketParameters(); + + if (marketParams.length == marketParameterIdx + 1) { + // if our index is the last market parameter, we can simply treat it as a single instance, + // and compute the phase according to the elapsed time from the beginning of the contract. + return _computePhase(marketParams[marketParameterIdx], block.timestamp - createdAt); + } else { + // we will accumulate the round from each phase, starting from the first one. + uint256 idx = marketParameterIdx; + // + // first iteration, we need to compute elapsed time from createdAt: + // createdAt -|- VVV | ... | ... | block.timestamp + (uint256 round,,) = _computePhase(marketParams[idx], marketParams[idx + 1].timestamp - createdAt); + idx++; + // start looking at all the intervals beginning from the respective market parameters index + // except for the last element, because we will compute the current phase and timeRemaining for it. + + while (idx < marketParams.length - 1) { + // for the intermediate elements we need the difference between their timestamps: + // createdAt | ... -|- VVV -|- ... | block.timestamp + (uint256 innerRound,,) = + _computePhase(marketParams[idx], marketParams[idx + 1].timestamp - marketParams[idx].timestamp); + + // accumulate rounds from each intermediate phase, along with a single offset round + round += innerRound + 1; + + idx++; + } + + // for last element we need to compute current phase and timeRemaining according + // to the elapsedTime at the last iteration, where we need to compute from the block.timestamp: + // createdAt | ... | ... | VVV -|- block.timestamp + (uint256 lastRound, Phase phase, uint256 timeRemaining) = + _computePhase(marketParams[idx], block.timestamp - marketParams[idx].timestamp); + // accumulate the last round as well, along with a single offset round + round += lastRound + 1; + return (round, phase, timeRemaining); + } + } + + /// @notice Function to set feeRoyalty. + /// @dev Only callable by the owner. + /// @dev Only callable in withdraw phase. + /// @param _fee new feeRoyalty, must be between 1 and 100. + function setFeeRoyalty(uint96 _fee) public onlyOwner { + _checkRoundPhase(Phase.Withdraw); + + if (_fee < 1 || _fee >= 100) { + revert InvalidFee(_fee); + } + royaltyFee = _fee; + } + + /// @notice Function to set the amountPerRound. + /// @dev Only callable by the owner. + /// @dev Only callable in withdraw phase. + /// @param _amountPerRound new amountPerRound. + function setAmountPerRound(uint256 _amountPerRound) external onlyOwner { + _checkRoundPhase(Phase.Withdraw); + + amountPerRound = _amountPerRound; + } +} diff --git a/contracts/swan/Swan.sol b/contracts/swan/Swan.sol new file mode 100644 index 0000000..5fdee95 --- /dev/null +++ b/contracts/swan/Swan.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: Apache-2.0 +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 "../llm/LLMOracleCoordinator.sol"; +import {LLMOracleTaskParameters} from "../llm/LLMOracleTask.sol"; +import {BuyerAgentFactory, BuyerAgent} from "./BuyerAgent.sol"; +import {SwanAssetFactory, SwanAsset} from "./SwanAsset.sol"; +import {SwanManager, SwanMarketParameters} from "./SwanManager.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +// 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"; + +contract Swan is SwanManager, UUPSUpgradeable, IERC721Receiver { + using SafeERC20 for ERC20; + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Invalid asset status. + error InvalidStatus(AssetStatus have, AssetStatus want); + + /// @notice Caller is not authorized for the operation, e.g. not a contract owner or listing owner. + error Unauthorized(address caller); + + /// @notice The given asset is still in the given round. + /// @dev Most likely coming from `relist` function, where the asset cant be + /// relisted in the same round that it was listed in. + error RoundNotFinished(address asset, uint256 round); + + /// @notice Asset count limit exceeded for this round + error AssetLimitExceeded(uint256 limit); + + /// @notice Invalid price for the asset. + error InvalidPrice(uint256 price); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice `asset` is created & listed for sale. + event AssetListed(address indexed owner, address indexed asset, uint256 price); + + /// @notice Asset relisted by it's `owner`. + /// @dev This may happen if a listed asset is not sold in the current round, and is relisted in a new round. + event AssetRelisted(address indexed owner, address indexed buyer, address indexed asset, uint256 price); + + /// @notice A `buyer` purchased an Asset. + event AssetSold(address indexed owner, address indexed buyer, address indexed asset, uint256 price); + + /// @notice A new buyer agent is created. + /// @dev `owner` is the owner of the buyer agent. + /// @dev `buyer` is the address of the buyer agent. + event BuyerCreated(address indexed owner, address indexed buyer); + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice Status of an asset. All assets are listed as soon as they are listed. + /// @dev Unlisted: cannot be purchased in the current round. + /// @dev Listed: can be purchase in the current round. + /// @dev Sold: asset is sold. + /// @dev It is important that `Unlisted` is only the default and is not set explicitly. + /// This allows to understand that if an asset is `Listed` but the round has past, it was not sold. + /// The said fact is used within the `relist` logic. + enum AssetStatus { + Unlisted, + Listed, + Sold + } + + /// @notice Holds the listing information. + /// @dev `createdAt` is the timestamp of the Asset creation. + /// @dev `royaltyFee` is the royaltyFee of the buyerAgent. + /// @dev `price` is the price of the Asset. + /// @dev `seller` is the address of the creator of the Asset. + /// @dev `buyer` is the address of the buyerAgent. + /// @dev `round` is the round in which the Asset is created. + /// @dev `status` is the status of the Asset. + struct AssetListing { + uint256 createdAt; + uint96 royaltyFee; + uint256 price; + address seller; // TODO: we can use asset.owner() instead of seller + address buyer; + uint256 round; + AssetStatus status; + } + + /// @notice To keep track of the assets for purchase. + mapping(address asset => AssetListing) public listings; + /// @notice Keeps track of assets per buyer & round. + mapping(address buyer => mapping(uint256 round => address[])) public assetsPerBuyerRound; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Locks the contract, preventing any future re-initialization. + /// @dev [See more](https://docs.openzeppelin.com/contracts/5.x/api/proxy#Initializable-_disableInitializers--). + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Function to receive ERC721 tokens via safe transfer. + /// @dev [See more](https://eips.ethereum.org/EIPS/eip-721). + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return this.onERC721Received.selector; + } + + /*////////////////////////////////////////////////////////////// + UPGRADABLE + //////////////////////////////////////////////////////////////*/ + + /// @notice Upgrades to contract with a new implementation. + /// @dev Only callable by the owner. + /// @param newImplementation address of the new implementation + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { + // `onlyOwner` modifier does the auth here + } + + /// @notice Initialize the contract. + function initialize( + SwanMarketParameters calldata _marketParameters, + LLMOracleTaskParameters calldata _oracleParameters, + // contracts + address _coordinator, + address _token, + address _buyerAgentFactory, + address _swanAssetFactory + ) public initializer { + __SwanManager_init(msg.sender); + + require(_marketParameters.platformFee <= 100, "Platform fee cannot exceed 100%"); + + // market & oracle parameters + marketParameters.push(_marketParameters); + oracleParameters = _oracleParameters; + + // contracts + coordinator = LLMOracleCoordinator(_coordinator); + token = ERC20(_token); + buyerAgentFactory = BuyerAgentFactory(_buyerAgentFactory); + swanAssetFactory = SwanAssetFactory(_swanAssetFactory); + + // swan is an operator + isOperator[address(this)] = true; + // owner is an operator + isOperator[msg.sender] = true; + } + + /*////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Creates a new Asset. + /// @param _name name of the token. + /// @param _symbol symbol of the token. + /// @param _desc description of the token. + /// @param _price price of the token. + /// @param _buyer address of the buyer. + function list(string calldata _name, string calldata _symbol, bytes calldata _desc, uint256 _price, address _buyer) + external + { + BuyerAgent buyer = BuyerAgent(_buyer); + (uint256 round, BuyerAgent.Phase phase,) = buyer.getRoundPhase(); + + // buyer must be in the sell phase + if (phase != BuyerAgent.Phase.Sell) { + revert BuyerAgent.InvalidPhase(phase, BuyerAgent.Phase.Sell); + } + // asset count must not exceed `maxAssetCount` + if (getCurrentMarketParameters().maxAssetCount == assetsPerBuyerRound[_buyer][round].length) { + revert AssetLimitExceeded(getCurrentMarketParameters().maxAssetCount); + } + // check the asset's price is within the acceptable range + if (_price < getCurrentMarketParameters().maxAssetCount || _price >= buyer.amountPerRound()) { + revert InvalidPrice(_price); + } + + // all is well, create the asset & its listing + address asset = address(swanAssetFactory.deploy(_name, _symbol, _desc, msg.sender)); + listings[asset] = AssetListing({ + createdAt: block.timestamp, + royaltyFee: buyer.royaltyFee(), + price: _price, + seller: msg.sender, + status: AssetStatus.Listed, + buyer: _buyer, + round: round + }); + + // add this to list of listings for the buyer for this round + assetsPerBuyerRound[_buyer][round].push(asset); + + // transfer royalties + transferRoyalties(listings[asset]); + + emit AssetListed(msg.sender, asset, _price); + } + + /// @notice Relist the asset for another round and/or another buyer and/or another price. + /// @param _asset address of the asset. + /// @param _buyer new buyerAgent for the asset. + /// @param _price new price of the token. + function relist(address _asset, address _buyer, uint256 _price) external { + AssetListing storage asset = listings[_asset]; + + // only the seller can relist the asset + if (asset.seller != msg.sender) { + revert Unauthorized(msg.sender); + } + + // asset must be listed + if (asset.status != AssetStatus.Listed) { + revert InvalidStatus(asset.status, AssetStatus.Listed); + } + + // relist can only happen after the round of its listing has ended + // we check this via the old buyer, that is the existing asset.buyer + // + // note that asset is unlisted here, but is not bought at all + // + // perhaps it suffices to check `==` here, since buyer round + // is changed incrementially + (uint256 oldRound,,) = BuyerAgent(asset.buyer).getRoundPhase(); + if (oldRound <= asset.round) { + revert RoundNotFinished(_asset, asset.round); + } + + // now we move on to the new buyer + BuyerAgent buyer = BuyerAgent(_buyer); + (uint256 round, BuyerAgent.Phase phase,) = buyer.getRoundPhase(); + + // buyer must be in sell phase + if (phase != BuyerAgent.Phase.Sell) { + revert BuyerAgent.InvalidPhase(phase, BuyerAgent.Phase.Sell); + } + + // buyer must not have more than `maxAssetCount` many assets + uint256 count = assetsPerBuyerRound[_buyer][round].length; + if (count >= getCurrentMarketParameters().maxAssetCount) { + revert AssetLimitExceeded(count); + } + + // create listing + listings[_asset] = AssetListing({ + createdAt: block.timestamp, + royaltyFee: buyer.royaltyFee(), + price: _price, + seller: msg.sender, + status: AssetStatus.Listed, + buyer: _buyer, + round: round + }); + + // add this to list of listings for the buyer for this round + assetsPerBuyerRound[_buyer][round].push(_asset); + + // transfer royalties + transferRoyalties(listings[_asset]); + + emit AssetRelisted(msg.sender, _buyer, _asset, _price); + } + + /// @notice Function to transfer the royalties to the seller & Dria. + function transferRoyalties(AssetListing storage asset) internal { + // calculate fees + uint256 buyerFee = (asset.price * asset.royaltyFee) / 100; + uint256 driaFee = (buyerFee * getCurrentMarketParameters().platformFee) / 100; + + // first, Swan receives the entire fee from seller + // this allows only one approval from the seller's side + token.safeTransferFrom(asset.seller, address(this), buyerFee); + + // send the buyer's portion to them + token.safeTransfer(asset.buyer, buyerFee - driaFee); + + // then it sends the remaining to Swan owner + token.safeTransfer(owner(), driaFee); + } + + /// @notice Executes the purchase of a listing for a buyer for the given asset. + /// @dev Must be called by the buyer of the given asset. + function purchase(address _asset) external { + AssetListing storage listing = listings[_asset]; + + // asset must be listed to be purchased + if (listing.status != AssetStatus.Listed) { + revert InvalidStatus(listing.status, AssetStatus.Listed); + } + + // can only the buyer can purchase the asset + if (listing.buyer != msg.sender) { + revert Unauthorized(msg.sender); + } + + // update asset status to be sold + listing.status = AssetStatus.Sold; + + // transfer asset from seller to Swan, and then from Swan to buyer + // this ensure that only approval to Swan is enough for the sellers + SwanAsset(_asset).safeTransferFrom(listing.seller, address(this), 1); + SwanAsset(_asset).safeTransferFrom(address(this), listing.buyer, 1); + + // transfer money + token.safeTransferFrom(listing.buyer, address(this), listing.price); + token.safeTransfer(listing.seller, listing.price); + + emit AssetSold(listing.seller, msg.sender, _asset, listing.price); + } + + /// @notice Returns the asset status with the given asset address. + /// @dev Active: If the asset has not been purchased or the next round has not started. + /// @dev Inactive: If the assets's purchaseRound has passed or delisted by the creator of the asset. + /// @dev Sold: If the asset has already been purchased by the buyer. + function getListingPrice(address _asset) external view returns (uint256) { + return listings[_asset].price; + } + + /// @notice Returns the number of assets with the given buyer and round. + /// @dev Assets can be assumed to be + function getListedAssets(address _buyer, uint256 _round) external view returns (address[] memory) { + return assetsPerBuyerRound[_buyer][_round]; + } + + /// @notice Returns the asset listing with the given asset address. + function getListing(address _asset) external view returns (AssetListing memory) { + return listings[_asset]; + } + + /// @notice Creates a new buyer agent. + /// @dev Emits a `BuyerCreated` event. + /// @return address of the new buyer agent. + function createBuyer( + string calldata _name, + string calldata _description, + uint96 _feeRoyalty, + uint256 _amountPerRound + ) external returns (BuyerAgent) { + BuyerAgent agent = buyerAgentFactory.deploy(_name, _description, _feeRoyalty, _amountPerRound, msg.sender); + emit BuyerCreated(msg.sender, address(agent)); + + return agent; + } +} diff --git a/contracts/swan/SwanAsset.sol b/contracts/swan/SwanAsset.sol new file mode 100644 index 0000000..b46bfd7 --- /dev/null +++ b/contracts/swan/SwanAsset.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @notice Factory contract to deploy SwanAsset tokens. +/// @dev This saves from contract space for Swan. +contract SwanAssetFactory { + /// @notice Deploys a new SwanAsset token. + function deploy(string memory _name, string memory _symbol, bytes memory _description, address _owner) + external + returns (SwanAsset) + { + return new SwanAsset(_name, _symbol, _description, _owner, msg.sender); + } +} + +/// @notice SwanAsset is an ERC721 token with a single token supply. +contract SwanAsset is ERC721, Ownable { + /// @notice Creation time of the token + uint256 public createdAt; + /// @notice Description of the token + bytes public description; + + /// @notice Constructor sets properties of the token. + constructor( + string memory _name, + string memory _symbol, + bytes memory _description, + address _owner, + address _operator + ) ERC721(_name, _symbol) Ownable(_owner) { + description = _description; + createdAt = block.timestamp; + + // owner is minted the token immediately + ERC721._safeMint(_owner, 1); + + // Swan (operator) is approved to by the owner immediately. + ERC721._setApprovalForAll(_owner, _operator, true); + } +} diff --git a/contracts/swan/SwanManager.sol b/contracts/swan/SwanManager.sol new file mode 100644 index 0000000..018e59f --- /dev/null +++ b/contracts/swan/SwanManager.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {LLMOracleCoordinator} from "../llm/LLMOracleCoordinator.sol"; +import {LLMOracleTaskParameters} from "../llm/LLMOracleTask.sol"; +import {BuyerAgentFactory, BuyerAgent} from "./BuyerAgent.sol"; +import {SwanAssetFactory, SwanAsset} from "./SwanAsset.sol"; + +/// @notice Collection of market-related parameters. +/// @dev Prevents stack-too-deep. +/// TODO: use 256-bit tight-packing here +struct SwanMarketParameters { + /// @notice The interval at which the buyerAgent can withdraw the funds. + uint256 withdrawInterval; + /// @notice The interval at which the creators can mint assets. + uint256 sellInterval; + /// @notice The interval at which the buyers can buy the assets. + uint256 buyInterval; + /// @notice A fee percentage taken from each listing's buyer fee. + uint256 platformFee; + /// @notice The maximum number of assets that can be listed per round. + uint256 maxAssetCount; + /// @notice Min asset price in the market. + uint256 minAssetPrice; + /// @notice Timestamp of the block that this market parameter was added. + /// @dev Even if this is provided by the user, it will get overwritten by the internal `block.timestamp`. + uint256 timestamp; +} + +abstract contract SwanManager is OwnableUpgradeable { + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice Market parameters such as intervals and fees. + SwanMarketParameters[] marketParameters; + /// @notice Oracle parameters such as fees. + LLMOracleTaskParameters oracleParameters; + + /// @notice Factory contract to deploy Buyer Agents. + BuyerAgentFactory public buyerAgentFactory; + /// @notice Factory contract to deploy SwanAsset tokens. + SwanAssetFactory public swanAssetFactory; + /// @notice LLM Oracle Coordinator. + LLMOracleCoordinator public coordinator; + /// @notice The token to be used for fee payments. + ERC20 public token; + + /// @notice Operator addresses that can take actions on behalf of Buyer agents, + /// such as calling `purchase`, or `updateState` for them. + mapping(address operator => bool) public isOperator; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + /// @notice Locks the contract, preventing any future re-initialization. + /// @dev [See more](https://docs.openzeppelin.com/contracts/5.x/api/proxy#Initializable-_disableInitializers--). + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function __SwanManager_init(address _owner) public onlyInitializing { + __Ownable_init(_owner); + } + + /*////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the market parameters in memory. + function getMarketParameters() external view returns (SwanMarketParameters[] memory) { + return marketParameters; + } + + /// @notice Returns the oracle parameters in memory. + function getOracleParameters() external view returns (LLMOracleTaskParameters memory) { + return oracleParameters; + } + + /// @notice Pushes a new market parameters to the marketParameters array. + /// @dev Only callable by owner. + /// @param _marketParameters new market parameters + function setMarketParameters(SwanMarketParameters memory _marketParameters) external onlyOwner { + require(_marketParameters.platformFee <= 100, "Platform fee cannot exceed 100%"); + _marketParameters.timestamp = block.timestamp; + marketParameters.push(_marketParameters); + } + + /// @notice Set the oracle parameters. + /// @dev Only callable by owner. + /// @param _oracleParameters new oracle parameters + function setOracleParameters(LLMOracleTaskParameters calldata _oracleParameters) external onlyOwner { + oracleParameters = _oracleParameters; + } + + /// @notice Returns the total fee required to make an oracle request. + /// @dev This is mainly required by the buyer to calculate its minimum fund amount, so that it can pay the fee. + function getOracleFee() external view returns (uint256) { + (uint256 totalFee,,) = coordinator.getFee(oracleParameters); + return totalFee; + } + /// @notice Set the factories for Buyer Agents and Swan Assets. + /// @dev Only callable by owner. + /// @param _buyerAgentFactory new BuyerAgentFactory address + /// @param _swanAssetFactory new SwanAssetFactory address + + function setFactories(address _buyerAgentFactory, address _swanAssetFactory) external onlyOwner { + buyerAgentFactory = BuyerAgentFactory(_buyerAgentFactory); + swanAssetFactory = SwanAssetFactory(_swanAssetFactory); + } + + /*////////////////////////////////////////////////////////////// + OPERATORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Adds an operator that can take actions on behalf of Buyer agents. + /// @dev Only callable by owner. + /// @dev Has no effect if the operator is already authorized. + /// @param _operator new operator address + function addOperator(address _operator) external onlyOwner { + isOperator[_operator] = true; + } + + /// @notice Removes an operator, so that they are no longer authorized. + /// @dev Only callable by owner. + /// @dev Has no effect if the operator is already not authorized. + /// @param _operator operator address to remove + function removeOperator(address _operator) external onlyOwner { + delete isOperator[_operator]; + } + + /// @notice Returns the current market parameters. + /// @dev Current market parameters = Last element in the marketParameters array + function getCurrentMarketParameters() public view returns (SwanMarketParameters memory) { + return marketParameters[marketParameters.length - 1]; + } +} diff --git a/contracts/test/Counter.t.sol b/contracts/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/contracts/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/contracts/token/WETH9.sol b/contracts/token/WETH9.sol new file mode 100644 index 0000000..20def46 --- /dev/null +++ b/contracts/token/WETH9.sol @@ -0,0 +1,755 @@ +// Copyright (C) 2015, 2016, 2017 Dapphub + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// pragma solidity >=0.4.22 <0.6; +// For not getting an "Bad CPU type in executable" error for solc-0.5.17 compiler + +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} + +/* + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +*/ diff --git a/coverage.sh b/coverage.sh new file mode 100644 index 0000000..d35078e --- /dev/null +++ b/coverage.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# exit on error +set -e + +# Run forge coverage +forge coverage \ + --report lcov \ + --report summary \ + --no-match-coverage "(test|mock|token)" + +# Install lcov +brew install lcov + +# Generate HTML report from lcov.info +genhtml lcov.info -o coverage --branch-coverage --ignore-errors inconsistent,category,corrupt \ No newline at end of file diff --git a/deployment/31337.json b/deployment/31337.json new file mode 100644 index 0000000..07a39ec --- /dev/null +++ b/deployment/31337.json @@ -0,0 +1,16 @@ +{ + "LLMOracleRegistry": { + "proxyAddr": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", + "implAddr": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512" + }, + "LLMOracleCoordinator": { + "proxyAddr": "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9", + "implAddr": "0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9" + }, + "Swan": { + "proxyAddr": "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6", + "implAddr": "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853" + }, + "BuyerAgentFactory": "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + "SwanAssetFactory": "0x0165878a594ca255338adfa4d48449f69242eb8f" +} \ No newline at end of file diff --git a/deployment/84532.json b/deployment/84532.json new file mode 100644 index 0000000..f6d115e --- /dev/null +++ b/deployment/84532.json @@ -0,0 +1,16 @@ +{ + "LLMOracleRegistry": { + "proxyAddr": "0x3ebea92509e87ba2168fa0150cbc334819ebc556", + "implAddr": "0xc1ae77f9aeecbd2ce3bedd7e7c95e1689e775fe6" + }, + "LLMOracleCoordinator": { + "proxyAddr": "0x3d6bf82ed76e18e358a8b15d1187c137d9d874c5", + "implAddr": "0xc7fa10912aecf712374b024878ac4bd7847476b1" + }, + "Swan": { + "proxyAddr": "0x2223fbcfc567abbad56ff1bbbf309ac3ea5e0fb9", + "implAddr": "0x6713d99e48164f2382a99875837067147a52ca28" + }, + "BuyerAgentFactory": "0x28c289d1c66479dd790b93cb905e8023e0fd4df9", + "SwanAssetFactory": "0x36ec62cfcbb69814b57f22fd7fbd182699978bcf" +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..a9f71ce --- /dev/null +++ b/foundry.toml @@ -0,0 +1,19 @@ +[profile.default] +src = 'contracts' +lib = 'lib' +test = 'test' +script = 'scripts' +out = 'out' +cache_path = 'cache' +ffi = true +ast = true +build_info = true +optimizer = true +extra_output = ['storageLayout'] +fs_permissions = [{ access = "read", path = "out" }, { access = "write", path = "deployment" }] +remappings = [ +"@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", +"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" +] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lcov.info b/lcov.info new file mode 100644 index 0000000..7257553 --- /dev/null +++ b/lcov.info @@ -0,0 +1,673 @@ +TN: +SF:contracts/libraries/Statistics.sol +FN:8,Statistics.avg +FNDA:13,Statistics.avg +DA:9,13 +DA:10,13 +DA:11,16 +DA:13,13 +FN:18,Statistics.variance +FNDA:13,Statistics.variance +DA:19,13 +DA:20,13 +DA:21,13 +DA:22,16 +DA:23,16 +DA:25,13 +FN:31,Statistics.stddev +FNDA:13,Statistics.stddev +DA:32,13 +DA:33,13 +DA:34,13 +FN:40,Statistics.sqrt +FNDA:13,Statistics.sqrt +DA:41,13 +DA:42,13 +DA:43,13 +DA:44,0 +DA:45,0 +FNF:4 +FNH:4 +LF:18 +LH:16 +BRF:0 +BRH:0 +end_of_record +TN: +SF:contracts/llm/LLMOracleCoordinator.sol +FN:81,LLMOracleCoordinator.onlyRegistered +FNDA:9,LLMOracleCoordinator.onlyRegistered +DA:82,9 +BRDA:82,0,0,- +DA:83,0 +FN:89,LLMOracleCoordinator.onlyAtStatus +FNDA:9,LLMOracleCoordinator.onlyAtStatus +DA:90,9 +BRDA:90,1,0,- +DA:91,0 +FN:103,LLMOracleCoordinator. +FNDA:39,LLMOracleCoordinator. +DA:104,39 +FN:113,LLMOracleCoordinator._authorizeUpgrade +FNDA:0,LLMOracleCoordinator._authorizeUpgrade +FN:124,LLMOracleCoordinator.initialize +FNDA:39,LLMOracleCoordinator.initialize +DA:131,39 +DA:132,39 +DA:133,39 +DA:134,39 +FN:149,LLMOracleCoordinator.request +FNDA:8,LLMOracleCoordinator.request +DA:155,8 +DA:158,8 +DA:159,8 +BRDA:159,2,0,- +DA:160,0 +DA:164,8 +DA:165,8 +BRDA:165,3,0,- +DA:166,0 +DA:170,8 +DA:173,8 +DA:175,8 +DA:177,8 +DA:180,8 +DA:191,8 +DA:193,8 +FN:206,LLMOracleCoordinator.respond +FNDA:15,LLMOracleCoordinator.respond +DA:211,11 +DA:214,11 +DA:215,3 +BRDA:215,4,0,1 +DA:216,1 +DA:221,10 +DA:224,10 +DA:226,10 +DA:229,10 +DA:232,10 +BRDA:232,5,0,2 +DA:233,2 +DA:237,10 +DA:238,8 +BRDA:238,6,0,8 +DA:239,8 +BRDA:239,7,0,1 +BRDA:239,7,1,7 +DA:241,1 +DA:242,1 +DA:245,7 +DA:246,7 +FN:259,LLMOracleCoordinator.validate +FNDA:9,LLMOracleCoordinator.validate +DA:264,9 +DA:267,9 +BRDA:267,8,0,- +DA:268,0 +DA:272,9 +DA:273,12 +BRDA:273,9,0,1 +DA:274,1 +DA:279,8 +DA:280,2 +BRDA:280,10,0,1 +DA:281,1 +DA:286,7 +DA:289,7 +DA:294,7 +DA:297,7 +DA:298,6 +BRDA:298,11,0,6 +DA:299,6 +DA:300,6 +DA:303,6 +FN:312,LLMOracleCoordinator.assertValidNonce +FNDA:17,LLMOracleCoordinator.assertValidNonce +DA:313,17 +DA:314,17 +BRDA:314,12,0,- +DA:315,0 +FN:322,LLMOracleCoordinator.finalizeValidation +FNDA:6,LLMOracleCoordinator.finalizeValidation +DA:323,6 +DA:326,6 +DA:328,7 +DA:329,7 +DA:330,9 +DA:334,7 +DA:338,7 +DA:339,7 +DA:340,7 +DA:341,9 +DA:342,9 +BRDA:342,13,0,9 +DA:343,9 +DA:344,9 +DA:347,9 +DA:352,7 +DA:353,7 +DA:358,6 +DA:359,6 +DA:360,7 +DA:364,6 +DA:365,6 +DA:367,7 +BRDA:367,14,0,7 +DA:368,7 +FN:374,LLMOracleCoordinator.withdrawPlatformFees +FNDA:0,LLMOracleCoordinator.withdrawPlatformFees +DA:375,0 +FN:381,LLMOracleCoordinator.getResponses +FNDA:0,LLMOracleCoordinator.getResponses +DA:382,0 +FN:388,LLMOracleCoordinator.getValidations +FNDA:0,LLMOracleCoordinator.getValidations +DA:389,0 +FN:395,LLMOracleCoordinator._increaseAllowance +FNDA:18,LLMOracleCoordinator._increaseAllowance +DA:396,18 +FN:403,LLMOracleCoordinator.getBestResponse +FNDA:5,LLMOracleCoordinator.getBestResponse +DA:404,5 +DA:407,5 +BRDA:407,15,0,- +DA:408,0 +DA:412,5 +DA:413,5 +DA:414,5 +DA:415,0 +BRDA:415,16,0,- +DA:416,0 +DA:417,0 +DA:421,5 +FNF:15 +FNH:11 +LF:97 +LH:84 +BRF:18 +BRH:10 +end_of_record +TN: +SF:contracts/llm/LLMOracleManager.sol +FN:47,LLMOracleManager.__LLMOracleManager_init +FNDA:39,LLMOracleManager.__LLMOracleManager_init +DA:51,39 +DA:52,39 +FN:55,LLMOracleManager.__LLMOracleManager_init_unchained +FNDA:39,LLMOracleManager.__LLMOracleManager_init_unchained +DA:59,39 +DA:60,39 +DA:62,39 +DA:63,39 +DA:65,39 +FN:73,LLMOracleManager.onlyValidParameters +FNDA:8,LLMOracleManager.onlyValidParameters +DA:75,8 +DA:76,0 +BRDA:76,0,0,- +DA:77,0 +DA:83,8 +DA:84,8 +DA:85,0 +BRDA:85,1,0,- +DA:86,0 +DA:92,8 +DA:93,8 +DA:94,0 +BRDA:94,2,0,- +DA:95,0 +FN:107,LLMOracleManager.setFees +FNDA:0,LLMOracleManager.setFees +DA:108,39 +DA:109,39 +DA:110,39 +FN:118,LLMOracleManager.getFee +FNDA:4,LLMOracleManager.getFee +DA:123,12 +DA:124,12 +DA:125,12 +DA:126,12 +FN:134,LLMOracleManager.setParameters +FNDA:0,LLMOracleManager.setParameters +DA:138,0 +DA:139,0 +FN:146,LLMOracleManager.setDeviationFactors +FNDA:0,LLMOracleManager.setDeviationFactors +DA:150,0 +DA:151,0 +FNF:7 +FNH:4 +LF:29 +LH:19 +BRF:3 +BRH:0 +end_of_record +TN: +SF:contracts/llm/LLMOracleRegistry.sol +FN:64,LLMOracleRegistry. +FNDA:47,LLMOracleRegistry. +DA:65,47 +FN:74,LLMOracleRegistry._authorizeUpgrade +FNDA:0,LLMOracleRegistry._authorizeUpgrade +FN:77,LLMOracleRegistry.initialize +FNDA:47,LLMOracleRegistry.initialize +DA:81,47 +DA:82,47 +DA:83,47 +DA:84,47 +FN:94,LLMOracleRegistry.register +FNDA:106,LLMOracleRegistry.register +DA:95,106 +DA:98,106 +BRDA:98,0,0,1 +DA:99,1 +DA:103,105 +BRDA:103,1,0,1 +DA:104,1 +DA:106,104 +DA:109,104 +DA:110,104 +FN:117,LLMOracleRegistry.unregister +FNDA:5,LLMOracleRegistry.unregister +DA:118,5 +DA:121,5 +BRDA:121,2,0,1 +DA:122,1 +DA:126,4 +DA:127,4 +DA:130,4 +FN:135,LLMOracleRegistry.setStakeAmounts +FNDA:0,LLMOracleRegistry.setStakeAmounts +DA:136,0 +DA:137,0 +FN:141,LLMOracleRegistry.getStakeAmount +FNDA:0,LLMOracleRegistry.getStakeAmount +DA:142,106 +FN:146,LLMOracleRegistry.isRegistered +FNDA:130,LLMOracleRegistry.isRegistered +DA:147,236 +FNF:8 +FNH:5 +LF:23 +LH:21 +BRF:3 +BRH:3 +end_of_record +TN: +SF:contracts/swan/BuyerAgent.sol +FN:13,BuyerAgentFactory.deploy +FNDA:62,BuyerAgentFactory.deploy +DA:20,62 +FN:106,BuyerAgent.onlyAuthorized +FNDA:1,BuyerAgent.onlyAuthorized +DA:108,1 +BRDA:108,0,0,- +DA:109,0 +FN:121,BuyerAgent. +FNDA:62,BuyerAgent. +DA:129,62 +BRDA:129,1,0,1 +DA:130,1 +DA:132,61 +DA:134,61 +DA:135,61 +DA:136,61 +DA:137,61 +DA:138,61 +DA:139,61 +DA:143,61 +DA:144,61 +FN:149,BuyerAgent.onERC721Received +FNDA:4,BuyerAgent.onERC721Received +DA:150,4 +FN:159,BuyerAgent.minFundAmount +FNDA:0,BuyerAgent.minFundAmount +DA:161,1 +FN:166,BuyerAgent.oracleResult +FNDA:0,BuyerAgent.oracleResult +DA:168,5 +BRDA:168,2,0,- +DA:169,0 +DA:172,5 +FN:182,BuyerAgent.oracleStateRequest +FNDA:1,BuyerAgent.oracleStateRequest +DA:184,1 +DA:186,1 +FN:197,BuyerAgent.oraclePurchaseRequest +FNDA:4,BuyerAgent.oraclePurchaseRequest +DA:199,4 +DA:201,4 +FN:208,BuyerAgent.updateState +FNDA:1,BuyerAgent.updateState +DA:210,1 +DA:213,1 +DA:214,0 +BRDA:214,3,0,- +DA:215,0 +DA:219,1 +DA:220,1 +DA:223,1 +FN:230,BuyerAgent.purchase +FNDA:6,BuyerAgent.purchase +DA:232,5 +DA:235,4 +DA:236,0 +BRDA:236,4,0,- +DA:237,0 +DA:241,4 +DA:242,4 +DA:245,4 +DA:246,5 +DA:249,5 +DA:250,5 +DA:251,5 +BRDA:251,5,0,1 +DA:252,1 +DA:256,4 +DA:259,4 +DA:263,3 +FN:270,BuyerAgent.withdraw +FNDA:3,BuyerAgent.withdraw +DA:271,2 +DA:275,2 +BRDA:275,6,0,1 +DA:278,1 +BRDA:278,7,0,1 +DA:279,1 +DA:284,1 +FN:289,BuyerAgent.treasury +FNDA:1,BuyerAgent.treasury +DA:290,2 +FN:295,BuyerAgent._checkRoundPhase +FNDA:18,BuyerAgent._checkRoundPhase +DA:296,18 +DA:297,18 +BRDA:297,8,0,3 +DA:298,3 +DA:301,15 +FN:308,BuyerAgent._computeCycleTime +FNDA:84,BuyerAgent._computeCycleTime +DA:309,84 +FN:316,BuyerAgent._computePhase +FNDA:84,BuyerAgent._computePhase +DA:321,84 +DA:322,84 +DA:323,84 +DA:328,84 +BRDA:328,9,0,49 +BRDA:328,9,1,13 +DA:329,49 +DA:330,35 +BRDA:330,10,0,22 +BRDA:330,10,1,13 +DA:331,22 +DA:333,13 +FN:342,BuyerAgent.getRoundPhase +FNDA:58,BuyerAgent.getRoundPhase +DA:343,78 +DA:345,78 +BRDA:345,11,0,73 +BRDA:345,11,1,5 +DA:348,73 +DA:351,5 +DA:355,5 +DA:356,5 +DA:360,6 +DA:363,1 +DA:364,1 +DA:367,1 +DA:369,1 +DA:375,5 +DA:376,5 +DA:378,5 +DA:379,5 +FN:387,BuyerAgent.setFeeRoyalty +FNDA:4,BuyerAgent.setFeeRoyalty +DA:388,4 +DA:390,3 +BRDA:390,12,0,2 +DA:391,2 +DA:393,1 +FN:400,BuyerAgent.setAmountPerRound +FNDA:3,BuyerAgent.setAmountPerRound +DA:401,3 +DA:403,2 +FNF:18 +FNH:16 +LF:85 +LH:79 +BRF:16 +BRH:12 +end_of_record +TN: +SF:contracts/swan/Swan.sol +FN:108,Swan. +FNDA:34,Swan. +DA:109,34 +FN:114,Swan.onERC721Received +FNDA:4,Swan.onERC721Received +DA:115,4 +FN:125,Swan._authorizeUpgrade +FNDA:0,Swan._authorizeUpgrade +FN:130,Swan.initialize +FNDA:34,Swan.initialize +DA:139,34 +DA:141,34 +BRDA:141,0,0,- +BRDA:141,0,1,34 +DA:144,34 +DA:145,34 +DA:148,34 +DA:149,34 +DA:150,34 +DA:151,34 +DA:154,34 +DA:156,34 +FN:169,Swan.list +FNDA:38,Swan.list +DA:172,38 +DA:173,38 +DA:176,38 +BRDA:176,1,0,1 +DA:177,1 +DA:180,37 +BRDA:180,2,0,1 +DA:181,1 +DA:184,36 +BRDA:184,3,0,- +DA:185,0 +DA:189,36 +DA:190,36 +DA:201,36 +DA:204,36 +DA:206,36 +FN:213,Swan.relist +FNDA:6,Swan.relist +DA:214,6 +DA:217,6 +BRDA:217,4,0,1 +DA:218,1 +DA:222,5 +BRDA:222,5,0,1 +DA:223,1 +DA:233,4 +DA:234,4 +BRDA:234,6,0,1 +DA:235,1 +DA:239,3 +DA:240,3 +DA:243,3 +BRDA:243,7,0,2 +DA:244,2 +DA:248,1 +DA:249,1 +BRDA:249,8,0,- +DA:250,0 +DA:254,1 +DA:265,1 +DA:268,1 +DA:270,1 +FN:274,Swan.transferRoyalties +FNDA:37,Swan.transferRoyalties +DA:276,37 +DA:277,37 +DA:281,37 +DA:284,37 +DA:287,37 +FN:292,Swan.purchase +FNDA:4,Swan.purchase +DA:293,4 +DA:296,4 +BRDA:296,9,0,- +DA:297,0 +DA:301,4 +BRDA:301,10,0,- +DA:302,0 +DA:306,4 +DA:310,4 +DA:311,4 +DA:314,4 +DA:315,4 +DA:317,4 +FN:324,Swan.getListingPrice +FNDA:5,Swan.getListingPrice +DA:325,5 +FN:330,Swan.getListedAssets +FNDA:22,Swan.getListedAssets +DA:331,22 +FN:335,Swan.getListing +FNDA:2,Swan.getListing +DA:336,2 +FN:342,Swan.createBuyer +FNDA:62,Swan.createBuyer +DA:348,62 +DA:349,61 +DA:351,61 +FNF:12 +FNH:11 +LF:66 +LH:62 +BRF:12 +BRH:7 +end_of_record +TN: +SF:contracts/swan/SwanAsset.sol +FN:11,SwanAssetFactory.deploy +FNDA:36,SwanAssetFactory.deploy +DA:15,36 +FN:27,SwanAsset. +FNDA:36,SwanAsset. +DA:34,36 +DA:35,36 +DA:38,36 +DA:41,36 +FNF:2 +FNH:2 +LF:5 +LH:5 +BRF:0 +BRH:0 +end_of_record +TN: +SF:contracts/swan/SwanManager.sol +FN:61,SwanManager. +FNDA:34,SwanManager. +DA:62,34 +FN:65,SwanManager.__SwanManager_init +FNDA:0,SwanManager.__SwanManager_init +DA:66,34 +FN:74,SwanManager.getMarketParameters +FNDA:141,SwanManager.getMarketParameters +DA:75,141 +FN:79,SwanManager.getOracleParameters +FNDA:6,SwanManager.getOracleParameters +DA:80,6 +FN:86,SwanManager.setMarketParameters +FNDA:3,SwanManager.setMarketParameters +DA:87,3 +BRDA:87,0,0,- +BRDA:87,0,1,3 +DA:88,3 +DA:89,3 +FN:95,SwanManager.setOracleParameters +FNDA:1,SwanManager.setOracleParameters +DA:96,1 +FN:101,SwanManager.getOracleFee +FNDA:1,SwanManager.getOracleFee +DA:102,1 +DA:103,1 +FN:110,SwanManager.setFactories +FNDA:1,SwanManager.setFactories +DA:111,1 +DA:112,1 +FN:123,SwanManager.addOperator +FNDA:0,SwanManager.addOperator +DA:124,0 +FN:131,SwanManager.removeOperator +FNDA:0,SwanManager.removeOperator +DA:132,0 +FN:137,SwanManager.getCurrentMarketParameters +FNDA:8,SwanManager.getCurrentMarketParameters +DA:138,120 +FNF:11 +FNH:8 +LF:15 +LH:13 +BRF:2 +BRH:1 +end_of_record +TN: +SF:script/Deploy.s.sol +FN:23,Deploy.run +FNDA:1,Deploy.run +DA:24,1 +DA:26,1 +DA:27,1 +DA:28,1 +DA:29,1 +DA:30,1 +FN:33,Deploy.deployLLM +FNDA:1,Deploy.deployLLM +DA:35,1 +DA:38,1 +DA:41,1 +DA:47,1 +DA:50,1 +DA:58,1 +FN:61,Deploy.deployFactories +FNDA:1,Deploy.deployFactories +DA:62,1 +DA:63,1 +FN:66,Deploy.deploySwan +FNDA:1,Deploy.deploySwan +DA:68,1 +DA:75,1 +DA:78,1 +DA:81,1 +DA:103,1 +FNF:4 +FNH:4 +LF:19 +LH:19 +BRF:0 +BRH:0 +end_of_record +TN: +SF:script/HelperConfig.s.sol +FN:32,HelperConfig. +FNDA:1,HelperConfig. +DA:34,1 +DA:35,1 +DA:36,1 +DA:38,1 +DA:49,1 +BRDA:49,0,0,- +DA:51,0 +DA:54,1 +FNF:1 +FNH:1 +LF:7 +LH:6 +BRF:1 +BRH:0 +end_of_record diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..c399975 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Script} from "../lib/forge-std/src/Script.sol"; +import {HelperConfig} from "../script/HelperConfig.s.sol"; +import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {LLMOracleRegistry} from "../contracts/llm/LLMOracleRegistry.sol"; +import {LLMOracleCoordinator, LLMOracleTaskParameters} from "../contracts/llm/LLMOracleCoordinator.sol"; +import {BuyerAgentFactory} from "../contracts/swan/BuyerAgent.sol"; +import {SwanAssetFactory} from "../contracts/swan/SwanAsset.sol"; +import {Swan, SwanMarketParameters} from "../contracts/swan/Swan.sol"; +import {Vm} from "../lib/forge-std/src/Vm.sol"; +import {Strings} from "../lib/openzeppelin-contracts/contracts/utils/Strings.sol"; + +contract Deploy is Script { + // contracts + LLMOracleCoordinator public oracleCoordinator; + LLMOracleRegistry public oracleRegistry; + BuyerAgentFactory public buyerAgentFactory; + SwanAssetFactory public swanAssetFactory; + Swan public swan; + + // implementation addresses + address registryImplementation; + address coordinatorImplementation; + address swanImplementation; + + HelperConfig public config; + uint256 chainId; + + function run() external { + chainId = block.chainid; + config = new HelperConfig(); + + vm.startBroadcast(); + deployLLM(); + deployFactories(); + deploySwan(); + vm.stopBroadcast(); + + writeContractAddresses(); + } + + function deployLLM() internal { + // get stakes + (uint256 genStake, uint256 valStake) = config.stakes(); + + // get fees + (uint256 platformFee, uint256 genFee, uint256 valFee) = config.fees(); + + // deploy llm contracts + address registryProxy = Upgrades.deployUUPSProxy( + "LLMOracleRegistry.sol", + abi.encodeCall(LLMOracleRegistry.initialize, (genStake, valStake, address(config.token()))) + ); + + // wrap proxy with the LLMOracleRegistry + oracleRegistry = LLMOracleRegistry(registryProxy); + registryImplementation = Upgrades.getImplementationAddress(registryProxy); + + // deploy coordinator contract + address coordinatorProxy = Upgrades.deployUUPSProxy( + "LLMOracleCoordinator.sol", + abi.encodeCall( + LLMOracleCoordinator.initialize, + (address(oracleRegistry), address(config.token()), platformFee, genFee, valFee) + ) + ); + + oracleCoordinator = LLMOracleCoordinator(coordinatorProxy); + coordinatorImplementation = Upgrades.getImplementationAddress(coordinatorProxy); + } + + function deployFactories() internal { + buyerAgentFactory = new BuyerAgentFactory(); + swanAssetFactory = new SwanAssetFactory(); + } + + function deploySwan() internal { + // get market params + ( + uint256 withdrawInterval, + uint256 sellInterval, + uint256 buyInterval, + uint256 platformFee, + uint256 maxAssetCount, + uint256 minAssetPrice, + ) = config.marketParams(); + + // get llm params + (uint8 diff, uint40 numGen, uint40 numVal) = config.taskParams(); + + // deploy swan + address swanProxy = Upgrades.deployUUPSProxy( + "Swan.sol", + abi.encodeCall( + Swan.initialize, + ( + SwanMarketParameters( + withdrawInterval, + sellInterval, + buyInterval, + platformFee, + maxAssetCount, + minAssetPrice, + block.timestamp + ), + LLMOracleTaskParameters(diff, numGen, numVal), + address(oracleCoordinator), + address(config.token()), + address(buyerAgentFactory), + address(swanAssetFactory) + ) + ) + ); + swan = Swan(swanProxy); + swanImplementation = Upgrades.getImplementationAddress(swanProxy); + } + + function writeContractAddresses() internal { + // create a deployment file if not exist + string memory fileName = Strings.toString(chainId); + string memory path = string.concat("deployment/", fileName, ".json"); + + string memory contracts = string.concat( + "{", + ' "LLMOracleRegistry": {', + ' "proxyAddr": "', Strings.toHexString(uint256(uint160(address(oracleRegistry))), 20), '",', + ' "implAddr": "', Strings.toHexString(uint256(uint160(address(registryImplementation))), 20), '"', + " },", + ' "LLMOracleCoordinator": {', + ' "proxyAddr": "', Strings.toHexString(uint256(uint160(address(oracleCoordinator))), 20), '",', + ' "implAddr": "', Strings.toHexString(uint256(uint160(address(coordinatorImplementation))), 20), '"', + " },", + ' "Swan": {', + ' "proxyAddr": "', Strings.toHexString(uint256(uint160(address(swan))), 20), '",', + ' "implAddr": "', Strings.toHexString(uint256(uint160(address(swanImplementation))), 20), '"', + " },", + ' "BuyerAgentFactory": "', Strings.toHexString(uint256(uint160(address(buyerAgentFactory))), 20), '",', + ' "SwanAssetFactory": "', Strings.toHexString(uint256(uint160(address(swanAssetFactory))), 20), '"' + "}" + ); + + vm.writeJson(contracts, path); + } +} diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol new file mode 100644 index 0000000..f6488a0 --- /dev/null +++ b/script/HelperConfig.s.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Script} from "../lib/forge-std/src/Script.sol"; +import {WETH9} from "../contracts/token/WETH9.sol"; +import {LLMOracleTaskParameters} from "../contracts/llm/LLMOracleTask.sol"; +import {SwanMarketParameters} from "../contracts/swan/SwanManager.sol"; + +struct Stakes { + uint256 generatorStakeAmount; + uint256 validatorStakeAmount; +} + +struct Fees { + uint256 platformFee; + uint256 generatorFee; + uint256 validatorFee; +} + +contract HelperConfig is Script { + LLMOracleTaskParameters public taskParams; + SwanMarketParameters public marketParams; + + Stakes public stakes; + Fees public fees; + WETH9 public token; + + // local key + uint256 public constant ANVIL_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + + 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}); + taskParams = LLMOracleTaskParameters({difficulty: 2, numGenerations: 1, numValidations: 1}); + + marketParams = SwanMarketParameters({ + maxAssetCount: 500, + sellInterval: 4 hours, + buyInterval: 30 minutes, + withdrawInterval: 15 minutes, + platformFee: 1, // percentage + minAssetPrice: 0.00001 ether, + timestamp: 0 // will be set in the first call + }); + + // for base sepolia + if (block.chainid == 84532) { + // use deployed weth + token = WETH9(payable(0x4200000000000000000000000000000000000006)); + } + // for local create a new token + token = new WETH9(); + } +} diff --git a/storage.sh b/storage.sh new file mode 100644 index 0000000..cb99842 --- /dev/null +++ b/storage.sh @@ -0,0 +1,21 @@ +#!/bin/bash +OUTPUT_PATH=${1:-storage} +EXCLUDE="test|mock|libraries|" + +IFS=$'\n' +CONTRACT_FILES=($(find ./contracts -type f)) +unset IFS + +echo "Generating layouts in $OUTPUT_PATH" +mkdir -p $OUTPUT_PATH + +for file in "${CONTRACT_FILES[@]}"; +do + if [[ $file =~ .*($EXCLUDE).* ]]; then + continue + fi + + contract=$(basename "$file" .sol) + echo "Generating storage layout of $contract" + forge inspect "$contract" storage --pretty > "$OUTPUT_PATH/$contract.md" +done \ No newline at end of file diff --git a/test/BuyerAgent.t.sol b/test/BuyerAgent.t.sol new file mode 100644 index 0000000..1584bb3 --- /dev/null +++ b/test/BuyerAgent.t.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Vm} from "../lib/forge-std/src/Vm.sol"; +import {Helper} from "./Helper.t.sol"; +import {WETH9} from "../contracts/token/WETH9.sol"; +import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {BuyerAgent, BuyerAgentFactory} from "../contracts/swan/BuyerAgent.sol"; +import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol"; +import {SwanAssetFactory} from "../contracts/swan/SwanAsset.sol"; +import {LLMOracleTaskParameters} from "../contracts/llm/LLMOracleTask.sol"; +import {LLMOracleRegistry} from "../contracts/llm/LLMOracleRegistry.sol"; +import {Swan} from "../contracts/swan/Swan.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract BuyerAgentTest is Helper { + address agentOwner; + BuyerAgent agent; + + modifier deployment() { + token = new WETH9(); + + // deploy llm contracts + vm.startPrank(dria); + + address registryProxy = Upgrades.deployUUPSProxy( + "LLMOracleRegistry.sol", + abi.encodeCall( + LLMOracleRegistry.initialize, (stakes.generatorStakeAmount, stakes.validatorStakeAmount, address(token)) + ) + ); + + oracleRegistry = LLMOracleRegistry(registryProxy); + + address coordinatorProxy = Upgrades.deployUUPSProxy( + "LLMOracleCoordinator.sol", + abi.encodeCall( + LLMOracleCoordinator.initialize, + (address(oracleRegistry), address(token), fees.platformFee, fees.generationFee, fees.validationFee) + ) + ); + oracleCoordinator = LLMOracleCoordinator(coordinatorProxy); + + // deploy factory contracts + buyerAgentFactory = new BuyerAgentFactory(); + swanAssetFactory = new SwanAssetFactory(); + + // deploy swan + address swanProxy = Upgrades.deployUUPSProxy( + "Swan.sol", + abi.encodeCall( + Swan.initialize, + ( + marketParameters, + oracleParameters, + address(oracleCoordinator), + address(token), + address(buyerAgentFactory), + address(swanAssetFactory) + ) + ) + ); + swan = Swan(swanProxy); + vm.stopPrank(); + + vm.label(address(swan), "Swan"); + vm.label(address(token), "WETH"); + vm.label(address(this), "BuyerAgentTest"); + vm.label(address(oracleRegistry), "LLMOracleRegistry"); + vm.label(address(oracleCoordinator), "LLMOracleCoordinator"); + vm.label(address(buyerAgentFactory), "BuyerAgentFactory"); + vm.label(address(swanAssetFactory), "SwanAssetFactory"); + _; + } + + modifier createBuyers() override { + for (uint256 i = 0; i < buyerAgentOwners.length; i++) { + // fund buyer agent owner + deal(address(token), buyerAgentOwners[i], 3 ether); + + vm.startPrank(buyerAgentOwners[i]); + BuyerAgent buyerAgent = swan.createBuyer( + buyerAgentParameters[i].name, + buyerAgentParameters[i].description, + buyerAgentParameters[i].royaltyFee, + buyerAgentParameters[i].amountPerRound + ); + + buyerAgents.push(buyerAgent); + vm.label(address(buyerAgent), string.concat("BuyerAgent#", vm.toString(i + 1))); + + // transfer tokens to agent + token.transfer(address(buyerAgent), amountPerRound); + assertEq(token.balanceOf(address(buyerAgent)), amountPerRound); + vm.stopPrank(); + } + + agentOwner = buyerAgentOwners[0]; + agent = buyerAgents[0]; + + currPhase = BuyerAgent.Phase.Sell; + currRound = 0; + _; + } + + /// @notice Buyer agent should be in sell phase + function test_InSellPhase() external deployment createBuyers { + // get curr phase + (, BuyerAgent.Phase _phase,) = agent.getRoundPhase(); + assertEq(uint8(_phase), uint8(currPhase)); + } + + /// @dev Agent owner cannot set feeRoyalty in sell phase + function test_RevertWhen_SetRoyaltyInSellPhase() external deployment createBuyers { + vm.prank(agentOwner); + vm.expectRevert( + abi.encodeWithSelector(BuyerAgent.InvalidPhase.selector, BuyerAgent.Phase.Sell, BuyerAgent.Phase.Withdraw) + ); + agent.setFeeRoyalty(10); + } + + /// @notice Test that the buyer agent is in buy phase + function test_InBuyPhase() external deployment createBuyers { + vm.warp(agent.createdAt() + marketParameters.sellInterval); + currPhase = BuyerAgent.Phase.Buy; + + (, BuyerAgent.Phase _phase,) = agent.getRoundPhase(); + assertEq(uint8(_phase), uint8(currPhase)); + } + + /// @dev Agent owner cannot set amountPerRound in buy phase + function test_RevertWhen_SetAmountPerRoundInBuyPhase() external deployment createBuyers { + vm.warp(agent.createdAt() + marketParameters.sellInterval); + + vm.prank(agentOwner); + vm.expectRevert( + abi.encodeWithSelector(BuyerAgent.InvalidPhase.selector, BuyerAgent.Phase.Buy, BuyerAgent.Phase.Withdraw) + ); + agent.setAmountPerRound(2 ether); + } + + /// @notice Test that the buyer agent owner cannot withdraw in buy phase + function test_RevertWhen_WithdrawInBuyPhase() external deployment createBuyers { + // owner cannot withdraw more than minFundAmount from his agent + vm.warp(agent.createdAt() + marketParameters.sellInterval); + + // get the contract balance + uint256 treasuary = agent.treasury(); + + vm.prank(agentOwner); + // try to withdraw all balance + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.MinFundSubceeded.selector, treasuary)); + agent.withdraw(uint96(treasuary)); + } + + /// @notice Test that the non-owner cannot withdraw + function test_RevertWhen_WithdrawByAnotherOwner() external deployment createBuyers { + // royalty fee can be set only in withdraw phase by only agent owner + vm.warp(agent.createdAt() + marketParameters.sellInterval + marketParameters.buyInterval); + currPhase = BuyerAgent.Phase.Withdraw; + + (, BuyerAgent.Phase _phase,) = agent.getRoundPhase(); + assertEq(uint8(_phase), uint8(currPhase)); + + // not allowed to withdraw by non owner + vm.prank(buyerAgentOwners[1]); + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.Unauthorized.selector, buyerAgentOwners[1])); + agent.withdraw(1 ether); + } + + /// @notice Test that the buyer agent owner must set feeRoyalty between 1-100 + function test_RevertWhen_SetFeeWithInvalidRoyalty() external deployment createBuyers { + vm.warp(agent.createdAt() + marketParameters.sellInterval + marketParameters.buyInterval); + + uint96 biggerRoyalty = 1000; + uint96 smallerRoyalty = 0; + + vm.startPrank(agentOwner); + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.InvalidFee.selector, biggerRoyalty)); + agent.setFeeRoyalty(biggerRoyalty); + + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.InvalidFee.selector, smallerRoyalty)); + agent.setFeeRoyalty(smallerRoyalty); + vm.stopPrank(); + } + + /// @notice Test that the buyer agent owner can set feeRoyalty and amountPerRound in withdraw phase + function test_SetRoyaltyAndAmountPerRound() external deployment createBuyers { + vm.warp(agent.createdAt() + marketParameters.sellInterval + marketParameters.buyInterval); + + uint96 newRoyaltyFee = 20; + uint256 newAmountPerRound = 0.25 ether; + + vm.startPrank(agentOwner); + agent.setFeeRoyalty(newRoyaltyFee); + agent.setAmountPerRound(newAmountPerRound); + + assertEq(agent.royaltyFee(), newRoyaltyFee); + assertEq(agent.amountPerRound(), newAmountPerRound); + + vm.stopPrank(); + } + + /// @notice Test that the buyer agent owner can withdraw in withdraw phase + function test_WithdrawInWithdrawPhase() external deployment createBuyers { + vm.warp(agent.createdAt() + marketParameters.sellInterval + marketParameters.buyInterval); + + vm.startPrank(agentOwner); + agent.withdraw(uint96(token.balanceOf(address(agent)))); + } +} diff --git a/test/Deploy.t.sol b/test/Deploy.t.sol new file mode 100644 index 0000000..91d43c8 --- /dev/null +++ b/test/Deploy.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +import {Deploy} from "../script/Deploy.s.sol"; +import {HelperConfig} from "../script/HelperConfig.s.sol"; +import {Test} from "../lib/forge-std/src/Test.sol"; +import {Vm} from "../lib/forge-std/src/Vm.sol"; +import {LLMOracleRegistry} from "../contracts/llm/LLMOracleRegistry.sol"; +import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol"; +import {Swan} from "../contracts/swan/Swan.sol"; + +pragma solidity ^0.8.20; + +contract DeployTest is Test { + Deploy deployer; + + LLMOracleCoordinator coordinator; + LLMOracleRegistry registry; + Swan swan; + + function setUp() external { + deployer = new Deploy(); + deployer.run(); + } + + modifier deployed() { + registry = deployer.oracleRegistry(); + coordinator = deployer.oracleCoordinator(); + swan = deployer.swan(); + + assert(address(registry) != address(0)); + assert(address(swan) != address(0)); + assert(address(coordinator) != address(0)); + + assert(coordinator.registry() == registry); + assert(swan.coordinator() == coordinator); + _; + } + + function test_Deploy() external deployed {} +} diff --git a/test/Helper.t.sol b/test/Helper.t.sol new file mode 100644 index 0000000..4c6277f --- /dev/null +++ b/test/Helper.t.sol @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Vm} from "../lib/forge-std/src/Vm.sol"; +import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {WETH9} from "../contracts/token/WETH9.sol"; +import {LLMOracleRegistry, LLMOracleKind} from "../contracts/llm/LLMOracleRegistry.sol"; +import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol"; +import {Test, console} from "../lib/forge-std/src/Test.sol"; +import {SwanMarketParameters} from "../contracts/swan/SwanManager.sol"; +import {LLMOracleTaskParameters} from "../contracts/llm/LLMOracleTask.sol"; +import {BuyerAgent} from "../contracts/swan/BuyerAgent.sol"; +import {Swan} from "../contracts/swan/Swan.sol"; +import {BuyerAgent, BuyerAgentFactory} from "../contracts/swan/BuyerAgent.sol"; +import {SwanAssetFactory, SwanAsset} from "../contracts/swan/SwanAsset.sol"; + +abstract contract Helper is Test { + struct Stakes { + uint256 generatorStakeAmount; + uint256 validatorStakeAmount; + } + + struct Fees { + uint256 platformFee; + uint256 generationFee; + uint256 validationFee; + } + + struct BuyerAgentParameters { + string name; + string description; + uint96 royaltyFee; + uint256 amountPerRound; + } + + bytes32 public constant ORACLE_PROTOCOL = "test/0.0.1"; + + Stakes stakes; + Fees fees; + + address dria; + address[] buyerAgentOwners; + address[] sellers; + + address[] generators; + address[] validators; + + uint256 currRound; + BuyerAgent.Phase currPhase; + + BuyerAgentParameters[] buyerAgentParameters; + LLMOracleTaskParameters oracleParameters; + SwanMarketParameters marketParameters; + + LLMOracleCoordinator oracleCoordinator; + LLMOracleRegistry oracleRegistry; + BuyerAgentFactory buyerAgentFactory; + SwanAssetFactory swanAssetFactory; + BuyerAgent[] buyerAgents; + + WETH9 token; + Swan swan; + + bytes input = "0x"; + bytes models = "0x"; + bytes metadata = "0x"; + + uint256 assetPrice = 0.01 ether; + uint256 amountPerRound = 0.015 ether; + uint96 royaltyFee = 2; + + uint256[] scores = [1 ether, 1 ether, 1 ether]; + + /// @notice The given nonce is not a valid proof-of-work. + error InvalidNonceFromHelperTest(uint256 taskId, uint256 nonce, uint256 computedNonce, address caller); + + function setUp() public { + dria = vm.addr(1); + validators = [vm.addr(2), vm.addr(3), vm.addr(4)]; + generators = [vm.addr(5), vm.addr(6), vm.addr(7)]; + buyerAgentOwners = [vm.addr(8), vm.addr(9)]; + sellers = [vm.addr(10), vm.addr(11)]; + + oracleParameters = LLMOracleTaskParameters({difficulty: 1, numGenerations: 1, numValidations: 1}); + marketParameters = SwanMarketParameters({ + withdrawInterval: 300, // 5 minutes + sellInterval: 360, + buyInterval: 600, + platformFee: 2, // percentage + maxAssetCount: 3, + timestamp: block.timestamp, + minAssetPrice: 0.00001 ether + }); + + stakes = Stakes({generatorStakeAmount: 0.01 ether, validatorStakeAmount: 0.01 ether}); + fees = Fees({platformFee: 0.0001 ether, generationFee: 0.0002 ether, validationFee: 0.00003 ether}); + + for (uint96 i = 0; i < buyerAgentOwners.length; i++) { + buyerAgentParameters.push( + BuyerAgentParameters({ + name: string.concat("BuyerAgent", vm.toString(uint256(i))), + description: "description of the buyer agent", + royaltyFee: royaltyFee, + amountPerRound: amountPerRound + }) + ); + + vm.label(buyerAgentOwners[i], string.concat("BuyerAgentOwner#", vm.toString(i + 1))); + } + vm.label(dria, "Dria"); + } + + modifier registerOracles() { + for (uint256 i = 0; i < generators.length; i++) { + // Approve the stake for the generator + vm.startPrank(generators[i]); + token.approve(address(oracleRegistry), stakes.generatorStakeAmount + stakes.validatorStakeAmount); + + // Register the generator oracle + oracleRegistry.register(LLMOracleKind.Generator); + vm.stopPrank(); + + assertTrue(oracleRegistry.isRegistered(generators[i], LLMOracleKind.Generator)); + vm.label(generators[i], string.concat("Generator#", vm.toString(i + 1))); + } + + for (uint256 i = 0; i < validators.length; i++) { + // Approve the stake for the validator + vm.startPrank(validators[i]); + token.approve(address(oracleRegistry), stakes.validatorStakeAmount); + + // Register the validator oracle + oracleRegistry.register(LLMOracleKind.Validator); + vm.stopPrank(); + + assertTrue(oracleRegistry.isRegistered(validators[i], LLMOracleKind.Validator)); + vm.label(validators[i], string.concat("Validator#", vm.toString(i + 1))); + } + _; + } + + modifier createBuyers() virtual { + for (uint256 i = 0; i < buyerAgentOwners.length; i++) { + // fund buyer agent owner + deal(address(token), buyerAgentOwners[i], 3 ether); + + // start recording event info + vm.recordLogs(); + + vm.startPrank(buyerAgentOwners[i]); + BuyerAgent buyerAgent = swan.createBuyer( + buyerAgentParameters[i].name, + buyerAgentParameters[i].description, + buyerAgentParameters[i].royaltyFee, + buyerAgentParameters[i].amountPerRound + ); + + // get recorded logs + Vm.Log[] memory entries = vm.getRecordedLogs(); + + // 1. OwnershipTransferred (from Ownable) + // 2. Approval (from BuyerAgent constructor to approve coordinator) + // 3. Approval (from BuyerAgent constructor to approve swan) + // 4. BuyerCreated (from Swan) + assertEq(entries.length, 4); + + // get the BuyerCreated event + Vm.Log memory buyerCreatedEvent = entries[entries.length - 1]; + + // Log is a struct that holds the event info: + // struct Log { + // bytes32[] topics; + // bytes data; + // address emitter; + // } + + // topics[0] is the event signature + // topics[1] is the first indexed parameter + // topics[2] is the second indexed parameter + // topics[3] is the third indexed parameter + // data holds non-indexed parameters (bytes) + // emitter is the address of the contract that emitted the event + + // get event sig + bytes32 eventSig = buyerCreatedEvent.topics[0]; + assertEq(keccak256("BuyerCreated(address,address)"), eventSig); + + // decode owner & agent address from topics + address owner = abi.decode(abi.encode(buyerCreatedEvent.topics[1]), (address)); + address agent = abi.decode(abi.encode(buyerCreatedEvent.topics[2]), (address)); + + // emitter should be swan + assertEq(buyerCreatedEvent.emitter, address(swan)); + + // all guuud + buyerAgents.push(BuyerAgent(agent)); + + vm.label(address(buyerAgents[i]), string.concat("BuyerAgent#", vm.toString(i + 1))); + + // transfer token to agent + token.transfer(address(buyerAgent), amountPerRound); + assertEq(token.balanceOf(address(buyerAgent)), amountPerRound); + vm.stopPrank(); + } + + assertEq(buyerAgents.length, buyerAgentOwners.length); + currPhase = BuyerAgent.Phase.Sell; + _; + } + + modifier sellersApproveToSwan() { + for (uint256 i = 0; i < sellers.length; i++) { + vm.prank(sellers[i]); + token.approve(address(swan), 1 ether); + assertEq(token.allowance(sellers[i], address(swan)), 1 ether); + } + _; + } + + modifier listAssets(address seller, uint256 assetCount, address buyerAgent) { + for (uint256 i = 0; i < assetCount; i++) { + vm.prank(seller); + swan.list( + string.concat("SwanAsset#", vm.toString(i)), + string.concat("SA#", vm.toString(i)), + "description or the swan asset", + assetPrice, + buyerAgent + ); + } + + // get listed assets + address[] memory listedAssets = swan.getListedAssets(buyerAgent, currRound); + assertEq(listedAssets.length, assetCount); + _; + } + + modifier setOracleParameters(uint8 _difficulty, uint40 _numGenerations, uint40 _numValidations) { + oracleParameters.difficulty = _difficulty; + oracleParameters.numGenerations = _numGenerations; + oracleParameters.numValidations = _numValidations; + + assertEq(oracleParameters.difficulty, _difficulty); + assertEq(oracleParameters.numGenerations, _numGenerations); + assertEq(oracleParameters.numValidations, _numValidations); + _; + } + + // check generator and validator allowances before and after function execution + // used in coordinator test + modifier checkAllowances() { + uint256[] memory generatorAllowancesBefore = new uint256[](oracleParameters.numGenerations); + uint256[] memory validatorAllowancesBefore; + + // get generator allowances before function execution + for (uint256 i = 0; i < oracleParameters.numGenerations; i++) { + generatorAllowancesBefore[i] = token.allowance(address(oracleCoordinator), generators[i]); + } + + // numValidations is greater than 0 + if (oracleParameters.numValidations > 0) { + validatorAllowancesBefore = new uint256[](oracleParameters.numValidations); + for (uint256 i = 0; i < oracleParameters.numValidations; i++) { + validatorAllowancesBefore[i] = token.allowance(address(oracleCoordinator), validators[i]); + } + // execute function + _; + + // validator allowances after function execution + (,,,,, uint256 valFee,,,) = oracleCoordinator.requests(1); + for (uint256 i = 0; i < oracleParameters.numValidations; i++) { + uint256 allowanceAfter = token.allowance(address(oracleCoordinator), validators[i]); + assertEq(allowanceAfter - validatorAllowancesBefore[i], valFee * oracleParameters.numGenerations); + } + } else { + // if no validations skip validator checks + _; + } + + // validate generator allowances after function execution + for (uint256 i = 0; i < oracleParameters.numGenerations; i++) { + uint256 allowanceAfter = token.allowance(address(oracleCoordinator), generators[i]); + (,,,, uint256 expectedIncrease,,,,) = oracleCoordinator.requests(1); + assertEq(allowanceAfter - generatorAllowancesBefore[i], expectedIncrease); + } + } + + // Mines a valid nonce until the hash meets the difficulty target + function mineNonce(address responder, uint256 taskId) internal view returns (uint256) { + // get the task + (address requester,,,,,,,,) = oracleCoordinator.requests(taskId); + uint256 target = type(uint256).max >> oracleParameters.difficulty; + + for (uint256 nonce; nonce < type(uint256).max; nonce++) { + bytes memory message = abi.encodePacked(taskId, input, requester, responder, nonce); + uint256 digest = uint256(keccak256(message)); + + if (uint256(digest) < target) { + return nonce; + } + } + } + + modifier safeRequest(address requester, uint256 taskId) { + (uint256 _total, uint256 _generator, uint256 _validator) = oracleCoordinator.getFee(oracleParameters); + + vm.startPrank(requester); // simulate transaction from requester + token.approve(address(oracleCoordinator), _total); + oracleCoordinator.request(ORACLE_PROTOCOL, input, models, oracleParameters); + vm.stopPrank(); + + // check request params + ( + address _requester, + , + , + , + uint256 _generatorFee, + uint256 _validatorFee, + , + bytes memory _input, + bytes memory _models + ) = oracleCoordinator.requests(taskId); + + assertEq(_requester, requester); + assertEq(_input, input); + assertEq(_models, models); + assertEq(_generatorFee, _generator); + assertEq(_validatorFee, _validator); + _; + } + + function safeRespond(address responder, bytes memory output, uint256 taskId) internal { + uint256 nonce = mineNonce(responder, taskId); + vm.prank(responder); + oracleCoordinator.respond(taskId, nonce, output, metadata); + } + + function safeValidate(address validator, uint256 taskId) internal { + uint256 nonce = mineNonce(validator, taskId); + vm.prank(validator); + oracleCoordinator.validate(taskId, nonce, scores, metadata); + } + + function safePurchase(address buyer, BuyerAgent buyerAgent, uint256 taskId) public { + address[] memory listedAssets = swan.getListedAssets(address(buyerAgent), currRound); + + // get the listed assets as output + address[] memory output = new address[](1); + output[0] = listedAssets[0]; + assertEq(output.length, 1); + + vm.prank(buyer); + buyerAgent.oraclePurchaseRequest(input, models); + + bytes memory encodedOutput = abi.encode((address[])(output)); + + // respond + safeRespond(generators[0], encodedOutput, taskId); + + // validate + safeValidate(validators[0], taskId); + + assert(token.balanceOf(address(buyerAgent)) > assetPrice); + + // purchase and check event logs + vm.recordLogs(); + vm.prank(buyer); + buyerAgent.purchase(); + } + + function setMarketParameters(SwanMarketParameters memory newMarketParameters) public { + vm.prank(dria); + swan.setMarketParameters(newMarketParameters); + + // get new params + SwanMarketParameters memory _newMarketParameters = swan.getCurrentMarketParameters(); + assertEq(_newMarketParameters.sellInterval, newMarketParameters.sellInterval); + assertEq(_newMarketParameters.buyInterval, newMarketParameters.buyInterval); + assertEq(_newMarketParameters.withdrawInterval, newMarketParameters.withdrawInterval); + } + + function checkRoundAndPhase(BuyerAgent agent, BuyerAgent.Phase phase, uint256 round) + public + view + returns (uint256) + { + // get the current round and phase of buyer agent + (uint256 _currRound, BuyerAgent.Phase _currPhase,) = agent.getRoundPhase(); + assertEq(uint8(_currPhase), uint8(phase)); + assertEq(uint8(_currRound), uint8(round)); + + // return the last timestamp to use in test + return block.timestamp; + } +} diff --git a/test/LLMOracleCoordinator.t.sol b/test/LLMOracleCoordinator.t.sol new file mode 100644 index 0000000..ce826f9 --- /dev/null +++ b/test/LLMOracleCoordinator.t.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {LLMOracleTask, LLMOracleTaskParameters} from "../contracts/llm/LLMOracleTask.sol"; +import {LLMOracleRegistry, LLMOracleKind} from "../contracts/llm/LLMOracleRegistry.sol"; +import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol"; +import {WETH9} from "../contracts/token/WETH9.sol"; +import {Vm} from "../lib/forge-std/src/Vm.sol"; +import {Helper} from "./Helper.t.sol"; + +contract LLMOracleCoordinatorTest is Helper { + address dummy = vm.addr(20); + address requester = vm.addr(21); + + bytes output = "0x"; + + modifier deployment() { + vm.startPrank(dria); + address registryProxy = Upgrades.deployUUPSProxy( + "LLMOracleRegistry.sol", + abi.encodeCall( + LLMOracleRegistry.initialize, (stakes.generatorStakeAmount, stakes.validatorStakeAmount, address(token)) + ) + ); + + // wrap proxy with the LLMOracleRegistry contract to use in tests easily + oracleRegistry = LLMOracleRegistry(registryProxy); + + // deploy coordinator contract + address coordinatorProxy = Upgrades.deployUUPSProxy( + "LLMOracleCoordinator.sol", + abi.encodeCall( + LLMOracleCoordinator.initialize, + (address(oracleRegistry), address(token), fees.platformFee, fees.generationFee, fees.validationFee) + ) + ); + oracleCoordinator = LLMOracleCoordinator(coordinatorProxy); + vm.stopPrank(); + + vm.label(dummy, "Dummy"); + vm.label(requester, "Requester"); + vm.label(address(this), "LLMOracleCoordinatorTest"); + vm.label(address(oracleRegistry), "LLMOracleRegistry"); + vm.label(address(oracleCoordinator), "LLMOracleCoordinator"); + _; + } + + modifier fund() { + // deploy weth + token = new WETH9(); + + // fund dria & requester + deal(address(token), dria, 1 ether); + deal(address(token), requester, 1 ether); + + // fund generators and validators + for (uint256 i = 0; i < generators.length; i++) { + deal(address(token), generators[i], stakes.generatorStakeAmount + stakes.validatorStakeAmount); + assertEq(token.balanceOf(generators[i]), stakes.generatorStakeAmount + stakes.validatorStakeAmount); + } + for (uint256 i = 0; i < validators.length; i++) { + deal(address(token), validators[i], stakes.validatorStakeAmount); + assertEq(token.balanceOf(validators[i]), stakes.validatorStakeAmount); + } + _; + } + + function test_Deployment() external fund deployment { + assertEq(oracleRegistry.generatorStakeAmount(), stakes.generatorStakeAmount); + assertEq(oracleRegistry.validatorStakeAmount(), stakes.validatorStakeAmount); + + assertEq(address(oracleRegistry.token()), address(token)); + assertEq(oracleRegistry.owner(), dria); + + // check the coordinator variables + assertEq(address(oracleCoordinator.feeToken()), address(token)); + assertEq(address(oracleCoordinator.registry()), address(oracleRegistry)); + assertEq(oracleCoordinator.platformFee(), fees.platformFee); + assertEq(oracleCoordinator.generationFee(), fees.generationFee); + assertEq(oracleCoordinator.validationFee(), fees.validationFee); + } + + /// @notice Test the registerOracles modifier to check if the oracles are registered + function test_RegisterOracles() external fund deployment registerOracles { + for (uint256 i; i < generators.length; i++) { + assertTrue(oracleRegistry.isRegistered(generators[i], LLMOracleKind.Generator)); + } + + for (uint256 i; i < validators.length; i++) { + assertTrue(oracleRegistry.isRegistered(validators[i], LLMOracleKind.Validator)); + } + } + + // @notice Test without validation + function test_WithoutValidation() + external + fund + setOracleParameters(1, 2, 0) + deployment + registerOracles + safeRequest(requester, 1) + checkAllowances + { + uint256 responseId; + + // try to respond as an outsider (should fail) + uint256 dummyNonce = mineNonce(dummy, 1); + vm.expectRevert(abi.encodeWithSelector(LLMOracleRegistry.NotRegistered.selector, dummy)); + vm.prank(dummy); + oracleCoordinator.respond(1, dummyNonce, output, metadata); + + // respond as the first generator + safeRespond(generators[0], output, 1); + + // verify the response + (address _responder,,, bytes memory _output,) = oracleCoordinator.responses(1, responseId); + assertEq(_responder, generators[0]); + assertEq(output, _output); + responseId++; + + // try responding again (should fail) + uint256 genNonce0 = mineNonce(generators[0], 1); + vm.expectRevert(abi.encodeWithSelector(LLMOracleCoordinator.AlreadyResponded.selector, 1, generators[0])); + vm.prank(generators[0]); + oracleCoordinator.respond(1, genNonce0, output, metadata); + + // second responder responds + safeRespond(generators[1], output, 1); + responseId++; + + // try to respond after task completion (should fail) + uint256 genNonce1 = mineNonce(generators[1], 1); + vm.expectRevert( + abi.encodeWithSelector( + LLMOracleCoordinator.InvalidTaskStatus.selector, + 1, + uint8(LLMOracleTask.TaskStatus.Completed), + uint8(LLMOracleTask.TaskStatus.PendingGeneration) + ) + ); + vm.prank(generators[1]); + oracleCoordinator.respond(1, genNonce1, output, metadata); + + // try to respond to a non-existent task (should fail) + vm.expectRevert( + abi.encodeWithSelector( + LLMOracleCoordinator.InvalidTaskStatus.selector, + 900, + uint8(LLMOracleTask.TaskStatus.None), + uint8(LLMOracleTask.TaskStatus.PendingGeneration) + ) + ); + vm.prank(generators[0]); + oracleCoordinator.respond(900, genNonce0, output, metadata); + } + + // @notice Test with single validation + function test_WithValidation() + external + fund + setOracleParameters(1, 2, 2) + deployment + registerOracles + safeRequest(requester, 1) + checkAllowances + { + // generators respond + for (uint256 i = 0; i < oracleParameters.numGenerations; i++) { + safeRespond(generators[i], output, 1); + } + + // set scores + scores = [1 ether, 1 ether]; + + uint256 genNonce = mineNonce(generators[2], 1); + // ensure third generator can't respond after completion + vm.expectRevert( + abi.encodeWithSelector( + LLMOracleCoordinator.InvalidTaskStatus.selector, + 1, + uint8(LLMOracleTask.TaskStatus.PendingValidation), + uint8(LLMOracleTask.TaskStatus.PendingGeneration) + ) + ); + vm.prank(generators[2]); + oracleCoordinator.respond(1, genNonce, output, metadata); + + // validator validate + safeValidate(validators[0], 1); + + uint256 valNonce = mineNonce(validators[0], 1); + // ensure first validator can't validate twice + vm.expectRevert(abi.encodeWithSelector(LLMOracleCoordinator.AlreadyResponded.selector, 1, validators[0])); + vm.prank(validators[0]); + oracleCoordinator.validate(1, valNonce, scores, metadata); + + // second validator validates and completes the task + safeValidate(validators[1], 1); + + // check the task's status is Completed + (,,, LLMOracleTask.TaskStatus status,,,,,) = oracleCoordinator.requests(1); + assertEq(uint8(status), uint8(LLMOracleTask.TaskStatus.Completed)); + + // should see generation scores + for (uint256 i = 0; i < oracleParameters.numGenerations; i++) { + (,, uint256 responseScore,,) = oracleCoordinator.responses(1, i); + assertEq(responseScore, 1 ether); + } + } + + /// @dev Oracle cannot validate if already participated as generator + function test_ValidatorIsGenerator() + external + fund + setOracleParameters(1, 1, 1) + deployment + registerOracles + safeRequest(requester, 1) + { + // register generators[0] as a validator as well + vm.prank(generators[0]); + oracleRegistry.register(LLMOracleKind.Validator); + + // respond as generator + for (uint256 i = 0; i < oracleParameters.numGenerations; i++) { + safeRespond(generators[i], output, 1); + } + + // set scores for (setOracleParameters(1, 1, 1)) + scores = [1 ether]; + + // try to validate after responding as generator + uint256 nonce = mineNonce(generators[0], 1); + vm.prank(generators[0]); + vm.expectRevert(abi.encodeWithSelector(LLMOracleCoordinator.AlreadyResponded.selector, 1, generators[0])); + oracleCoordinator.validate(1, nonce, scores, metadata); + } +} diff --git a/test/LLMOracleRegistry.t.sol b/test/LLMOracleRegistry.t.sol new file mode 100644 index 0000000..4ec0a74 --- /dev/null +++ b/test/LLMOracleRegistry.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Vm} from "../lib/forge-std/src/Vm.sol"; +import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {WETH9} from "../contracts/token/WETH9.sol"; +import {LLMOracleRegistry, LLMOracleKind} from "../contracts/llm/LLMOracleRegistry.sol"; +import {Helper} from "./Helper.t.sol"; + +contract LLMOracleRegistryTest is Helper { + uint256 totalStakeAmount; + address oracle; + + modifier deployment() { + oracle = generators[0]; + totalStakeAmount = stakes.generatorStakeAmount + stakes.validatorStakeAmount; + + token = new WETH9(); + + vm.startPrank(dria); + address registryProxy = Upgrades.deployUUPSProxy( + "LLMOracleRegistry.sol", + abi.encodeCall( + LLMOracleRegistry.initialize, (stakes.generatorStakeAmount, stakes.validatorStakeAmount, address(token)) + ) + ); + + // wrap proxy with the LLMOracleRegistry contract to use in tests easily + oracleRegistry = LLMOracleRegistry(registryProxy); + vm.stopPrank(); + + vm.label(oracle, "Oracle"); + vm.label(address(this), "LLMOracleRegistryTest"); + vm.label(address(oracleRegistry), "LLMOracleRegistry"); + vm.label(address(oracleCoordinator), "LLMOracleCoordinator"); + _; + } + + /// @notice fund the oracle and dria + modifier fund() { + deal(address(token), dria, 1 ether); + deal(address(token), oracle, totalStakeAmount); + + assertEq(token.balanceOf(dria), 1 ether); + assertEq(token.balanceOf(oracle), totalStakeAmount); + _; + } + + // move to helper ? + modifier registerOracle(LLMOracleKind kind) { + // register oracle + vm.startPrank(oracle); + token.approve(address(oracleRegistry), totalStakeAmount); + + // Register the generator oracle + oracleRegistry.register(kind); + vm.stopPrank(); + _; + } + + // move to helper ? + modifier unregisterOracle(LLMOracleKind kind) { + // Simulate the oracle account + vm.startPrank(oracle); + token.approve(address(oracleRegistry), stakes.generatorStakeAmount); + oracleRegistry.unregister(kind); + vm.stopPrank(); + + assertFalse(oracleRegistry.isRegistered(oracle, LLMOracleKind.Generator)); + _; + } + + function test_Deployment() external deployment { + assertEq(oracleRegistry.generatorStakeAmount(), stakes.generatorStakeAmount); + assertEq(oracleRegistry.validatorStakeAmount(), stakes.validatorStakeAmount); + + assertEq(address(oracleRegistry.token()), address(token)); + assertEq(oracleRegistry.owner(), dria); + } + + /// @notice Registry has not approved by oracle + function test_RevertWhen_RegistryHasNotApprovedByOracle() external deployment { + // oracle has the funds but has not approved yet + deal(address(token), oracle, totalStakeAmount); + + vm.expectRevert(abi.encodeWithSelector(LLMOracleRegistry.InsufficientFunds.selector)); + oracleRegistry.register(LLMOracleKind.Generator); + } + + /// @notice Oracle has enough funds and approve registry + function test_RegisterGeneratorOracle() external deployment fund registerOracle(LLMOracleKind.Generator) {} + + /// @notice Same oracle try to register twice + function test_RevertWhen_RegisterSameGeneratorTwice() + external + deployment + fund + registerOracle(LLMOracleKind.Generator) + { + vm.prank(oracle); + vm.expectRevert(abi.encodeWithSelector(LLMOracleRegistry.AlreadyRegistered.selector, oracle)); + + oracleRegistry.register(LLMOracleKind.Generator); + } + + /// @notice Oracle registers as validator + function test_RegisterValidatorOracle() external deployment fund registerOracle(LLMOracleKind.Validator) {} + + /// @notice Oracle unregisters as generator + function test_UnregisterOracle() + external + deployment + fund + registerOracle(LLMOracleKind.Generator) + unregisterOracle(LLMOracleKind.Generator) + {} + + /// @notice Oracle try to unregisters as generator twice + function test_RevertWhen_UnregisterSameGeneratorTwice() + external + deployment + fund + registerOracle(LLMOracleKind.Generator) + unregisterOracle(LLMOracleKind.Generator) + { + vm.prank(oracle); + vm.expectRevert(abi.encodeWithSelector(LLMOracleRegistry.NotRegistered.selector, oracle)); + oracleRegistry.unregister(LLMOracleKind.Generator); + } + + /// @notice Oracle can withdraw stakes after unregistering + /// @dev 1. Register as generator + /// @dev 2. Register as validator + /// @dev 3. Unregister as generator + /// @dev 4. Unregister as validator + /// @dev 5. withdraw stakes + function test_WithdrawStakesAfterUnregistering() + external + deployment + fund + registerOracle(LLMOracleKind.Generator) + registerOracle(LLMOracleKind.Validator) + unregisterOracle(LLMOracleKind.Generator) + unregisterOracle(LLMOracleKind.Validator) + { + uint256 balanceBefore = token.balanceOf(oracle); + token.approve(address(oracleRegistry), totalStakeAmount); + + // withdraw stakes + vm.startPrank(oracle); + token.transferFrom(address(oracleRegistry), oracle, (totalStakeAmount)); + + uint256 balanceAfter = token.balanceOf(oracle); + assertEq(balanceAfter - balanceBefore, totalStakeAmount); + } +} diff --git a/test/Swan.t.sol b/test/Swan.t.sol new file mode 100644 index 0000000..01cd9ab --- /dev/null +++ b/test/Swan.t.sol @@ -0,0 +1,568 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {LLMOracleTaskParameters} from "../contracts/llm/LLMOracleTask.sol"; +import {LLMOracleRegistry} from "../contracts/llm/LLMOracleRegistry.sol"; +import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol"; +import {BuyerAgent, BuyerAgentFactory} from "../contracts/swan/BuyerAgent.sol"; +import {SwanAssetFactory, SwanAsset} from "../contracts/swan/SwanAsset.sol"; +import {Swan, SwanMarketParameters} from "../contracts/swan/Swan.sol"; +import {WETH9} from "../contracts/token/WETH9.sol"; +import {Vm} from "../lib/forge-std/src/Vm.sol"; +import {Helper} from "./Helper.t.sol"; + +contract SwanTest is Helper { + modifier deployment() { + token = new WETH9(); + + // deploy llm contracts + vm.startPrank(dria); + + address registryProxy = Upgrades.deployUUPSProxy( + "LLMOracleRegistry.sol", + abi.encodeCall( + LLMOracleRegistry.initialize, (stakes.generatorStakeAmount, stakes.validatorStakeAmount, address(token)) + ) + ); + oracleRegistry = LLMOracleRegistry(registryProxy); + + address coordinatorProxy = Upgrades.deployUUPSProxy( + "LLMOracleCoordinator.sol", + abi.encodeCall( + LLMOracleCoordinator.initialize, + (address(oracleRegistry), address(token), fees.platformFee, fees.generationFee, fees.validationFee) + ) + ); + oracleCoordinator = LLMOracleCoordinator(coordinatorProxy); + + // deploy factory contracts + buyerAgentFactory = new BuyerAgentFactory(); + swanAssetFactory = new SwanAssetFactory(); + + // deploy swan + address swanProxy = Upgrades.deployUUPSProxy( + "Swan.sol", + abi.encodeCall( + Swan.initialize, + ( + marketParameters, + oracleParameters, + address(oracleCoordinator), + address(token), + address(buyerAgentFactory), + address(swanAssetFactory) + ) + ) + ); + swan = Swan(swanProxy); + vm.stopPrank(); + + vm.label(address(swan), "Swan"); + vm.label(address(token), "WETH"); + vm.label(address(this), "SwanTest"); + vm.label(address(oracleRegistry), "LLMOracleRegistry"); + vm.label(address(oracleCoordinator), "LLMOracleCoordinator"); + vm.label(address(buyerAgentFactory), "BuyerAgentFactory"); + vm.label(address(swanAssetFactory), "SwanAssetFactory"); + _; + } + + modifier fund() { + scores = [1 ether]; + + // fund dria + deal(address(token), dria, 1 ether); + + // fund generators + for (uint256 i; i < generators.length; i++) { + deal(address(token), generators[i], stakes.generatorStakeAmount); + assertEq(token.balanceOf(generators[i]), stakes.generatorStakeAmount); + } + // fund validators + for (uint256 i; i < validators.length; i++) { + deal(address(token), validators[i], stakes.validatorStakeAmount); + assertEq(token.balanceOf(validators[i]), stakes.validatorStakeAmount); + } + // 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))); + } + _; + } + + function test_Deployment() external deployment fund { + assertEq(oracleCoordinator.platformFee(), fees.platformFee); + assertEq(oracleCoordinator.generationFee(), fees.generationFee); + assertEq(oracleCoordinator.validationFee(), fees.validationFee); + + assertEq(oracleRegistry.generatorStakeAmount(), stakes.generatorStakeAmount); + assertEq(oracleRegistry.validatorStakeAmount(), stakes.validatorStakeAmount); + assertEq(swan.owner(), dria); + } + + function test_CreateBuyerAgents() external deployment createBuyers fund { + assertEq(buyerAgents.length, buyerAgentOwners.length); + + for (uint256 i = 0; i < buyerAgents.length; i++) { + assertEq(buyerAgents[i].royaltyFee(), buyerAgentParameters[i].royaltyFee); + assertEq(buyerAgents[i].owner(), buyerAgentOwners[i]); + assertEq(buyerAgents[i].amountPerRound(), buyerAgentParameters[i].amountPerRound); + assertEq(buyerAgents[i].name(), buyerAgentParameters[i].name); + assertEq(token.balanceOf(address(buyerAgents[i])), buyerAgentParameters[i].amountPerRound); + } + } + + /// @notice Sellers cannot list more than maxAssetCount + function test_RevertWhen_ListMoreThanMaxAssetCount() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + // try to list more than max assets + vm.prank(sellers[0]); + vm.expectRevert(abi.encodeWithSelector(Swan.AssetLimitExceeded.selector, marketParameters.maxAssetCount)); + swan.list("name", "symbol", "desc", 0.001 ether, address(buyerAgents[0])); + } + + /// @notice Buyer cannot call purchase() in sell phase + function test_RevertWhen_PurchaseInSellPhase() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + // try to purchase + vm.prank(buyerAgentOwners[0]); + vm.expectRevert( + abi.encodeWithSelector(BuyerAgent.InvalidPhase.selector, BuyerAgent.Phase.Sell, BuyerAgent.Phase.Buy) + ); + buyerAgents[0].purchase(); + } + + /// @notice Seller cannot relist the asset in the same round (for same or different buyers) + function test_RevertWhen_RelistInTheSameRound() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + // get the listed asset + address assetToFail = swan.getListedAssets(address(buyerAgents[0]), currRound)[0]; + + vm.prank(SwanAsset(assetToFail).owner()); + vm.expectRevert(abi.encodeWithSelector(Swan.RoundNotFinished.selector, assetToFail, currRound)); + swan.relist(assetToFail, address(buyerAgents[1]), 0.001 ether); + } + + /// @notice Buyer cannot purchase an asset that is not listed for him + function test_RevertWhen_PurchaseByAnotherBuyer() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + address buyerToFail = buyerAgentOwners[0]; + BuyerAgent buyerAgent = buyerAgents[1]; + + vm.warp(buyerAgents[0].createdAt() + marketParameters.sellInterval); + currPhase = BuyerAgent.Phase.Buy; + + vm.expectRevert(abi.encodeWithSelector(Swan.Unauthorized.selector, buyerToFail)); + vm.prank(buyerToFail); + buyerAgent.purchase(); + } + + /// @notice Buyer cannot spend more than amountPerRound per round + function test_RevertWhen_PurchaseMoreThanAmountPerRound() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + address buyerToFail = buyerAgentOwners[0]; + BuyerAgent buyerAgentToFail = buyerAgents[0]; + + vm.warp(buyerAgentToFail.createdAt() + marketParameters.sellInterval); + currPhase = BuyerAgent.Phase.Buy; + + // get the listed assets as output + address[] memory output = swan.getListedAssets(address(buyerAgentToFail), currRound); + bytes memory encodedOutput = abi.encode(output); + + vm.prank(buyerToFail); + // make a purchase request + buyerAgentToFail.oraclePurchaseRequest(input, models); + + // respond + safeRespond(generators[0], encodedOutput, 1); + + // validate + safeValidate(validators[0], 1); + + vm.prank(buyerToFail); + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.BuyLimitExceeded.selector, assetPrice * 2, amountPerRound)); + buyerAgentToFail.purchase(); + } + + /// @notice Buyer can purchase + /// @dev Seller has to approve Swan + /// @dev Buyer Agent must be in buy phase + /// @dev Buyer Agent must have enough balance to purchase + /// @dev asset price must be less than amountPerRound + function test_PurchaseAnAsset() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + // increase time to buy phase to be able to purchase + vm.warp(buyerAgents[0].createdAt() + marketParameters.sellInterval); + safePurchase(buyerAgentOwners[0], buyerAgents[0], 1); + + // 1. Transfer (from Swan) + // 2. Transfer (from Swan) + // 3. Transfer (from WETH9) + // 4. Transfer (from WETH9) + // 5. AssetSold + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 5); + + // get the AssetSold event + Vm.Log memory assetSoldEvent = entries[entries.length - 1]; + + // check event sig + bytes32 eventSig = assetSoldEvent.topics[0]; + assertEq(keccak256("AssetSold(address,address,address,uint256)"), eventSig); + + // 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 asset = abi.decode(abi.encode(assetSoldEvent.topics[3]), (address)); + uint256 price = abi.decode(assetSoldEvent.data, (uint256)); + + assertEq(agent, address(buyerAgents[0])); + assertEq(asset, buyerAgents[0].inventory(0, 0)); + + // get asset details + Swan.AssetListing memory assetListing = swan.getListing(asset); + + assertEq(assetListing.seller, seller); + assertEq(assetListing.buyer, address(buyerAgents[0])); + + assertEq(uint8(assetListing.status), uint8(Swan.AssetStatus.Sold)); + assertEq(assetListing.price, price); + + // emitter should be swan + assertEq(assetSoldEvent.emitter, address(swan)); + } + + /// @notice Buyer can update his state after purchase + function test_UpdateState() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + address buyerAgentOwner = buyerAgentOwners[0]; + BuyerAgent buyerAgent = buyerAgents[0]; + uint256 taskId = 1; + + vm.warp(buyerAgent.createdAt() + marketParameters.sellInterval); + currPhase = BuyerAgent.Phase.Buy; + + safePurchase(buyerAgentOwners[0], buyerAgents[0], taskId); + vm.warp(buyerAgents[0].createdAt() + marketParameters.sellInterval + marketParameters.buyInterval); + taskId++; + + bytes memory newState = abi.encodePacked("0x", "after purchase"); + + vm.prank(buyerAgentOwner); + buyerAgents[0].oracleStateRequest(input, models); + + safeRespond(generators[0], newState, taskId); + safeValidate(validators[0], taskId); + + vm.prank(buyerAgentOwner); + buyerAgent.updateState(); + assertEq(buyerAgent.state(), newState); + } + + /// @notice Seller cannot list an asset in withdraw phase + function test_RevertWhen_ListInWithdrawPhase() external deployment fund createBuyers sellersApproveToSwan { + vm.warp(buyerAgents[0].createdAt() + marketParameters.sellInterval + marketParameters.buyInterval); + currPhase = BuyerAgent.Phase.Withdraw; + + vm.prank(sellers[0]); + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.InvalidPhase.selector, currPhase, BuyerAgent.Phase.Sell)); + swan.list("name", "symbol", "desc", 0.01 ether, address(buyerAgents[0])); + } + + /// @notice Buyer Agent Owner can setAmountPerRound in withdraw phase + function test_SetAmountPerRound() external deployment fund createBuyers sellersApproveToSwan { + vm.warp(buyerAgents[0].createdAt() + marketParameters.sellInterval + marketParameters.buyInterval); + + uint256 newAmountPerRound = 2 ether; + + vm.prank(buyerAgentOwners[0]); + buyerAgents[0].setAmountPerRound(newAmountPerRound); + assertEq(buyerAgents[0].amountPerRound(), newAmountPerRound); + } + + /// @notice Buyer Agent Owner cannot create buyer agent with invalid royalty + /// @dev feeRoyalty must be between 0 - 100 + function test_RevertWhen_CreateBuyerWithInvalidRoyalty() external deployment fund { + uint96 invalidRoyalty = 150; + + vm.prank(buyerAgentOwners[0]); + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.InvalidFee.selector, invalidRoyalty)); + swan.createBuyer( + buyerAgentParameters[0].name, + buyerAgentParameters[0].description, + invalidRoyalty, + buyerAgentParameters[0].amountPerRound + ); + } + + /// @notice Swan owner can set factories + function test_SetFactories() external deployment fund { + SwanAssetFactory _swanAssetFactory = new SwanAssetFactory(); + BuyerAgentFactory _buyerAgentFactory = new BuyerAgentFactory(); + + vm.prank(dria); + swan.setFactories(address(_buyerAgentFactory), address(_swanAssetFactory)); + + assertEq(address(swan.buyerAgentFactory()), address(_buyerAgentFactory)); + assertEq(address(swan.swanAssetFactory()), address(_swanAssetFactory)); + } + + /// @notice Seller cannot relist an asset that is already purchased + function test_RevertWhen_RelistAlreadyPurchasedAsset() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + address buyer = buyerAgentOwners[0]; + BuyerAgent buyerAgent = buyerAgents[0]; + uint256 taskId = 1; + + // increase time to buy phase + vm.warp(buyerAgent.createdAt() + marketParameters.sellInterval); + + safePurchase(buyer, buyerAgent, taskId); + + // increase time to the sell phase of the next round + vm.warp(buyerAgent.createdAt() + marketParameters.buyInterval + marketParameters.withdrawInterval); + + // get the asset + address listedAssetAddr = swan.getListedAssets(address(buyerAgent), currRound)[0]; + assertEq(buyerAgent.inventory(currRound, 0), listedAssetAddr); + + Swan.AssetListing memory asset = swan.getListing(listedAssetAddr); + + // try to relist the asset + vm.prank(sellers[0]); + vm.expectRevert(abi.encodeWithSelector(Swan.InvalidStatus.selector, asset.status, Swan.AssetStatus.Listed)); + swan.relist(listedAssetAddr, address(buyerAgent), asset.price); + } + + /// @notice Seller cannot relist another seller's asset + function test_RevertWhen_RelistByAnotherSeller() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + BuyerAgent buyerAgent = buyerAgents[0]; + address listedAssetAddr = swan.getListedAssets(address(buyerAgent), currRound)[0]; + + // increase time to the sell phase of the next round + vm.warp( + buyerAgent.createdAt() + marketParameters.sellInterval + marketParameters.buyInterval + + marketParameters.withdrawInterval + ); + + // try to relist an asset by another seller + vm.prank(sellers[1]); + vm.expectRevert(abi.encodeWithSelector(Swan.Unauthorized.selector, sellers[1])); + swan.relist(listedAssetAddr, address(buyerAgent), 0.1 ether); + } + + /// @notice Seller can relist an asset + /// @dev Buyer Agent must be in Sell Phase + function test_RelistAsset() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + BuyerAgent buyerAgent = buyerAgents[0]; + BuyerAgent buyerAgentToRelist = buyerAgents[1]; + + address listedAssetAddr = swan.getListedAssets(address(buyerAgent), currRound)[0]; + + // increase time to the sell phase of the next round + vm.warp( + buyerAgent.createdAt() + marketParameters.sellInterval + marketParameters.buyInterval + + marketParameters.withdrawInterval + ); + + // try to relist an asset by another seller + vm.recordLogs(); + vm.prank(sellers[0]); + swan.relist(listedAssetAddr, address(buyerAgentToRelist), assetPrice); + + // check the logs + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 4); + // Transfer (from WETH) + // Transfer (from WETH) + // Transfer (from WETH) + // AssetRelisted + + // get the event data + Vm.Log memory assetRelistedEvent = entries[entries.length - 1]; + + bytes32 eventSig = assetRelistedEvent.topics[0]; + assertEq(keccak256("AssetRelisted(address,address,address,uint256)"), eventSig); + + address owner = abi.decode(abi.encode(assetRelistedEvent.topics[1]), (address)); + address agent = abi.decode(abi.encode(assetRelistedEvent.topics[2]), (address)); + address asset = abi.decode(abi.encode(assetRelistedEvent.topics[3]), (address)); + uint256 price = abi.decode(assetRelistedEvent.data, (uint256)); + + assertEq(owner, sellers[0]); + assertEq(agent, address(buyerAgentToRelist)); + assertEq(asset, listedAssetAddr); + assertEq(price, assetPrice); + } + + /// @notice Seller cannot relist an asset in Buy Phase + function test_RevertWhen_RelistInBuyPhase() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + BuyerAgent buyerAgent = buyerAgents[0]; + address listedAssetAddr = swan.getListedAssets(address(buyerAgent), currRound)[0]; + + uint256 cycleTime = + marketParameters.buyInterval + marketParameters.sellInterval + marketParameters.withdrawInterval; + + vm.warp(buyerAgent.createdAt() + cycleTime + marketParameters.sellInterval); + currPhase = BuyerAgent.Phase.Buy; + + // try to relist + vm.prank(sellers[0]); + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.InvalidPhase.selector, currPhase, BuyerAgent.Phase.Sell)); + swan.relist(listedAssetAddr, address(buyerAgent), 0.1 ether); + } + + /// @notice Seller cannot relist an asset in Withdraw Phase + function test_RevertWhen_RelistInWithdrawPhase() + external + deployment + fund + createBuyers + sellersApproveToSwan + registerOracles + listAssets(sellers[0], marketParameters.maxAssetCount, address(buyerAgents[0])) + { + BuyerAgent buyerAgent = buyerAgents[0]; + address listedAssetAddr = swan.getListedAssets(address(buyerAgent), currRound)[0]; + + uint256 cycleTime = + marketParameters.buyInterval + marketParameters.sellInterval + marketParameters.withdrawInterval; + + vm.warp(buyerAgent.createdAt() + cycleTime + marketParameters.sellInterval + marketParameters.buyInterval); + currPhase = BuyerAgent.Phase.Withdraw; + + // try to relist + vm.prank(sellers[0]); + vm.expectRevert(abi.encodeWithSelector(BuyerAgent.InvalidPhase.selector, currPhase, BuyerAgent.Phase.Sell)); + swan.relist(listedAssetAddr, address(buyerAgent), 0.1 ether); + } + + /// @notice Swan owner can set market parameters + /// @dev Only Swan owner can set market parameters + function test_SetMarketParameters() external deployment fund createBuyers { + vm.warp(buyerAgents[0].createdAt() + marketParameters.buyInterval + marketParameters.sellInterval); + + SwanMarketParameters memory newMarketParameters = SwanMarketParameters({ + withdrawInterval: 10 * 60, + sellInterval: 12 * 60, + buyInterval: 20 * 60, + platformFee: 12, + maxAssetCount: 100, + timestamp: block.timestamp, + minAssetPrice: 0.00001 ether + }); + + vm.prank(dria); + swan.setMarketParameters(newMarketParameters); + + SwanMarketParameters memory updatedParams = swan.getCurrentMarketParameters(); + assertEq(updatedParams.withdrawInterval, newMarketParameters.withdrawInterval); + assertEq(updatedParams.sellInterval, newMarketParameters.sellInterval); + assertEq(updatedParams.buyInterval, newMarketParameters.buyInterval); + assertEq(updatedParams.platformFee, newMarketParameters.platformFee); + assertEq(updatedParams.maxAssetCount, newMarketParameters.maxAssetCount); + assertEq(updatedParams.timestamp, newMarketParameters.timestamp); + } + + /// @notice Swan owner can set oracle parameters + /// @dev Only Swan owner can set oracle parameters + function test_SetOracleParameters() external deployment fund createBuyers { + vm.warp(buyerAgents[0].createdAt() + marketParameters.buyInterval + marketParameters.sellInterval); + + LLMOracleTaskParameters memory newOracleParameters = + LLMOracleTaskParameters({difficulty: 5, numGenerations: 3, numValidations: 4}); + + vm.prank(dria); + swan.setOracleParameters(newOracleParameters); + + LLMOracleTaskParameters memory updatedParams = swan.getOracleParameters(); + + assertEq(updatedParams.difficulty, newOracleParameters.difficulty); + assertEq(updatedParams.numGenerations, newOracleParameters.numGenerations); + assertEq(updatedParams.numValidations, newOracleParameters.numValidations); + } +} diff --git a/test/SwanIntervals.t.sol b/test/SwanIntervals.t.sol new file mode 100644 index 0000000..451e84f --- /dev/null +++ b/test/SwanIntervals.t.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {LLMOracleTaskParameters} from "../contracts/llm/LLMOracleTask.sol"; +import {LLMOracleRegistry} from "../contracts/llm/LLMOracleRegistry.sol"; +import {Upgrades} from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol"; +import {BuyerAgent, BuyerAgentFactory} from "../contracts/swan/BuyerAgent.sol"; +import {SwanAssetFactory, SwanAsset} from "../contracts/swan/SwanAsset.sol"; +import {Swan, SwanMarketParameters} from "../contracts/swan/Swan.sol"; +import {WETH9} from "../contracts/token/WETH9.sol"; +import {Vm} from "../lib/forge-std/src/Vm.sol"; +import {Helper} from "./Helper.t.sol"; + +contract SwanIntervalsTest is Helper { + modifier deployment() { + token = new WETH9(); + + // deploy llm contracts + vm.startPrank(dria); + + address registryProxy = Upgrades.deployUUPSProxy( + "LLMOracleRegistry.sol", + abi.encodeCall( + LLMOracleRegistry.initialize, (stakes.generatorStakeAmount, stakes.validatorStakeAmount, address(token)) + ) + ); + + oracleRegistry = LLMOracleRegistry(registryProxy); + + address coordinatorProxy = Upgrades.deployUUPSProxy( + "LLMOracleCoordinator.sol", + abi.encodeCall( + LLMOracleCoordinator.initialize, + (address(oracleRegistry), address(token), fees.platformFee, fees.generationFee, fees.validationFee) + ) + ); + oracleCoordinator = LLMOracleCoordinator(coordinatorProxy); + + // deploy factory contracts + buyerAgentFactory = new BuyerAgentFactory(); + swanAssetFactory = new SwanAssetFactory(); + + // deploy swan + address swanProxy = Upgrades.deployUUPSProxy( + "Swan.sol", + abi.encodeCall( + Swan.initialize, + ( + marketParameters, + oracleParameters, + address(oracleCoordinator), + address(token), + address(buyerAgentFactory), + address(swanAssetFactory) + ) + ) + ); + swan = Swan(swanProxy); + vm.stopPrank(); + + // label contracts to be able to identify them easily in console + vm.label(address(swan), "Swan"); + vm.label(address(token), "WETH"); + vm.label(address(this), "SwanIntervalsTest"); + vm.label(address(oracleRegistry), "LLMOracleRegistry"); + vm.label(address(oracleCoordinator), "LLMOracleCoordinator"); + vm.label(address(buyerAgentFactory), "BuyerAgentFactory"); + vm.label(address(swanAssetFactory), "SwanAssetFactory"); + _; + } + + /// @notice Check the current phase is Sell right after creation of buyer agent + function test_InSellPhase() external deployment createBuyers { + // agent should be in sell phase right after creation + checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Sell, 0); + } + + /// @notice Check the current phase is Buy increase time to buy phase + function test_InBuyPhase() external deployment createBuyers { + vm.warp(buyerAgents[0].createdAt() + swan.getCurrentMarketParameters().sellInterval); + checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Buy, 0); + } + + /// @notice Check the current phase is Withdraw after increase time to withdraw phase + function test_InWithdrawPhase() external deployment createBuyers { + vm.warp( + buyerAgents[0].createdAt() + swan.getCurrentMarketParameters().sellInterval + + swan.getCurrentMarketParameters().buyInterval + ); + checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Withdraw, 0); + } + + /// @notice Change the intervals and check the current phase and round is are correct + function test_ChangeCycleTime() external deployment createBuyers { + // increase time to buy phase of the second round + vm.warp(buyerAgents[0].createdAt() + swan.getCurrentMarketParameters().sellInterval); + checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Buy, 0); + + // decrease cycle time + setMarketParameters( + SwanMarketParameters({ + withdrawInterval: 60, + sellInterval: 600, + buyInterval: 120, + platformFee: 2, + maxAssetCount: 3, + timestamp: block.timestamp, + minAssetPrice: 0.00001 ether + }) + ); + + // get all params + SwanMarketParameters[] memory allParams = swan.getMarketParameters(); + assertEq(allParams.length, 2); + + // should be in sell phase of second round after set + uint256 currTimestamp = checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Sell, 1); + + // increase time to buy phase of the second round + vm.warp(currTimestamp + swan.getCurrentMarketParameters().sellInterval); + checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Buy, 1); + + // deploy new buyer agent + vm.prank(buyerAgentOwners[0]); + BuyerAgent agentAfterFirstSet = swan.createBuyer( + buyerAgentParameters[1].name, + buyerAgentParameters[1].description, + buyerAgentParameters[1].royaltyFee, + buyerAgentParameters[1].amountPerRound + ); + + // buyerAgents[0] should be in buy phase of second round + checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Buy, 1); + + // agentAfterFirstSet should be in sell phase of the first round + checkRoundAndPhase(agentAfterFirstSet, BuyerAgent.Phase.Sell, 0); + + // increase cycle time + setMarketParameters( + SwanMarketParameters({ + withdrawInterval: 600, + sellInterval: 1000, + buyInterval: 10, + platformFee: 2, // percentage + maxAssetCount: 3, + timestamp: block.timestamp, + minAssetPrice: 0.00001 ether + }) + ); + + // get all params + allParams = swan.getMarketParameters(); + assertEq(allParams.length, 3); + + // buyerAgents[0] should be in sell phase of the third round + checkRoundAndPhase(buyerAgents[0], BuyerAgent.Phase.Sell, 2); + + // agentAfterFirstSet should be in sell phase of the second round + checkRoundAndPhase(agentAfterFirstSet, BuyerAgent.Phase.Sell, 1); + } +}