From 141d91124a9b4cbc5ef5c71fa2500ca9b2d6c203 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 15 May 2024 15:03:13 +0800 Subject: [PATCH 01/30] upgraded staking contract --- contracts/timelock/TimeLock.sol | 205 +++++++++++ contracts/timelock/TimeLockStaking.sol | 317 ------------------ contracts/timelock/base/BaseToken.sol | 56 +--- .../timelock/interfaces/ITimeLockDeposits.sol | 2 +- hardhat.config.js | 5 +- scripts/arguments/lockArguments.js | 1 + scripts/arguments/stakingArguments.js | 14 - scripts/deployTimeLock.ts | 7 +- 8 files changed, 229 insertions(+), 378 deletions(-) create mode 100644 contracts/timelock/TimeLock.sol delete mode 100644 contracts/timelock/TimeLockStaking.sol create mode 100644 scripts/arguments/lockArguments.js delete mode 100644 scripts/arguments/stakingArguments.js diff --git a/contracts/timelock/TimeLock.sol b/contracts/timelock/TimeLock.sol new file mode 100644 index 0000000..d582a0a --- /dev/null +++ b/contracts/timelock/TimeLock.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "./base/BaseToken.sol"; +import "./interfaces/ITimeLockDeposits.sol"; + +contract TimeLock is BaseToken, ITimeLockDeposits { + using Math for uint256; + using SafeERC20 for IERC20; + + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + bytes32 public constant GOV_ROLE = keccak256("GOV_ROLE"); + + uint256 public constant MIN_LOCK_DURATION = 10 minutes; + bool public isAllowPositionStaking = true; + bool public isAllowDeposits = true; + bool public isAdminUnlock = false; + + address _treasury; // Recipient of withdrawal fees + + mapping(address => Deposit[]) public depositsOf; + + struct Deposit { + uint256 amount; + uint64 start; + uint64 end; + address token; + bool cancelled; + uint16 fees; // Cancellation fees in percentage with divisor 10000 + } + + constructor( + string memory _name, + string memory _symbol, + address treasury_ + ) BaseToken(_name, _symbol) { + _treasury = treasury_; + } + + event Deposited( + uint256 amount, + uint256 duration, + address indexed receiver, + address indexed from, + address token + ); + + event Withdrawn( + uint256 amount, + uint256 indexed depositId, + address indexed receiver, + address indexed from, + address token + ); + + event Cancelled(address indexed from, address token, uint16 fees); + + function deposit( + uint256 _amount, + uint256 _duration, + address _receiver, + address _token + ) external { + require(isAllowDeposits, "Cannot deposit now"); + require(_amount > 0, "Cannot deposit 0"); + + IERC20(_token).safeTransferFrom(_msgSender(), address(this), _amount); + + depositsOf[_receiver].push( + Deposit({ + amount: _amount, + start: uint64(block.timestamp), + end: uint64(block.timestamp) + uint64(_duration), + token: _token, + cancelled: false, + fees: 0 + }) + ); + + _mint(_receiver, _amount); + emit Deposited(_amount, _duration, _receiver, _msgSender(), _token); + } + + function withdraw(uint256 _depositId, address _receiver) external { + require( + _depositId < depositsOf[_msgSender()].length, + "Deposit does not exist" + ); + Deposit memory userDeposit = depositsOf[_msgSender()][_depositId]; + if (!isAdminUnlock) { + require(block.timestamp >= userDeposit.end, "Too soon"); + } + + // User must have enough to wthdraw + require( + balanceOf(_msgSender()) >= userDeposit.amount, + "User does not have enough Staking Tokens to withdraw" + ); + // remove Deposit + depositsOf[_msgSender()][_depositId] = depositsOf[_msgSender()][ + depositsOf[_msgSender()].length - 1 + ]; + depositsOf[_msgSender()].pop(); + + // burn pool shares + _burn(_msgSender(), userDeposit.amount); + + // return tokens + IERC20(userDeposit.token).safeTransfer(_receiver, userDeposit.amount); + emit Withdrawn( + userDeposit.amount, + _depositId, + _receiver, + _msgSender(), + userDeposit.token + ); + } + + // Force withdrawing a deposit as proposed by DAO + function forceWithdraw( + address _token, + address _account + ) external onlyRole(ADMIN_ROLE) { + for (uint256 i = 0; i < depositsOf[_account].length; i++) { + Deposit memory userDeposit = depositsOf[_account][i]; + if (userDeposit.token == _token && userDeposit.cancelled == true) { + uint256 fees = (userDeposit.amount * userDeposit.fees) / 10000; + uint256 amount = userDeposit.amount - fees; + depositsOf[_account][i] = depositsOf[_account][ + depositsOf[_account].length - 1 + ]; + depositsOf[_account].pop(); + _burn(_account, userDeposit.amount); + IERC20(userDeposit.token).safeTransfer(_treasury, fees); + IERC20(userDeposit.token).safeTransfer(_account, amount); + emit Withdrawn( + userDeposit.amount, + i, + _account, + _account, + userDeposit.token + ); + break; + } + } + } + + function cancelDeposit( + address _token, + address _account, + uint16 _fees + ) external onlyRole(GOV_ROLE) { + for (uint256 i = 0; i < depositsOf[_account].length; i++) { + Deposit storage userDeposit = depositsOf[_account][i]; + // Leave out the cancelled == false check to allow for fees adjustment + if (userDeposit.token == _token) { + userDeposit.cancelled = true; + userDeposit.fees = _fees; + emit Cancelled(_account, _token, _fees); + break; + } + } + } + + function getTotalDeposit(address _account) public view returns (uint256) { + uint256 total; + for (uint256 i = 0; i < depositsOf[_account].length; i++) { + total += depositsOf[_account][i].amount; + } + + return total; + } + + function getDepositsOf( + address _account + ) public view returns (Deposit[] memory) { + return depositsOf[_account]; + } + + function getDepositsOfLength( + address _account + ) public view returns (uint256) { + return depositsOf[_account].length; + } + + function adjustPositionStaking() external onlyRole(ADMIN_ROLE) { + isAllowPositionStaking = !isAllowPositionStaking; + } + + function adjustDeposits() external onlyRole(ADMIN_ROLE) { + isAllowDeposits = !isAllowDeposits; + } + + function adjustAdminUnlock() external onlyRole(ADMIN_ROLE) { + isAdminUnlock = !isAdminUnlock; + } + + function adjustTreasury(address treasury_) external onlyRole(ADMIN_ROLE) { + _treasury = treasury_; + } +} diff --git a/contracts/timelock/TimeLockStaking.sol b/contracts/timelock/TimeLockStaking.sol deleted file mode 100644 index 1f0d2a0..0000000 --- a/contracts/timelock/TimeLockStaking.sol +++ /dev/null @@ -1,317 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/utils/math/Math.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "./base/BaseToken.sol"; -import "./interfaces/ITimeLockDeposits.sol"; - -contract TimeLockStaking is BaseToken, ITimeLockDeposits { - using Math for uint256; - using SafeERC20 for IERC20; - - uint256 public maxBonus; - uint256 public maxLockDuration; - uint256 public constant MIN_LOCK_DURATION = 10 minutes; - bool public isAllowPositionStaking = true; - bool public isAllowDeposits = true; - bool public isAdminUnlock = false; - uint256[] public curve; - uint256 public unit; - - error MaxBonusError(); - error ShortCurveError(); - error CurveIncreaseError(); - - mapping(address => Deposit[]) public depositsOf; - - struct Deposit { - uint256 amount; - uint256 shareAmount; - uint64 start; - uint64 end; - } - - constructor( - string memory _name, - string memory _symbol, - address _depositToken, - uint256 _maxBonus, - uint256 _maxLockDuration, - uint256[] memory _curve - ) BaseToken(_name, _symbol, _depositToken) { - require( - _maxLockDuration >= MIN_LOCK_DURATION, - "TimeLockPool.constructor: max lock duration must be greater or equal to mininmum lock duration" - ); - maxBonus = _maxBonus; - maxLockDuration = _maxLockDuration; - checkCurve(_curve); - for (uint i = 0; i < _curve.length; i++) { - if (_curve[i] > _maxBonus) { - revert MaxBonusError(); - } - curve.push(_curve[i]); - } - unit = _maxLockDuration / (curve.length - 1); - } - - event Deposited( - uint256 amount, - uint256 shareAmount, - uint256 duration, - address indexed receiver, - address indexed from - ); - event Withdrawn( - uint256 indexed depositId, - address indexed receiver, - address indexed from, - uint256 amount - ); - event CurveChanged(address indexed sender); - - function deposit( - uint256 _amount, - uint256 _duration, - address _receiver - ) external override { - require(isAllowDeposits, "Cannot deposit now"); - require(_amount > 0, "cannot deposit 0"); - // Don't allow locking > maxLockDuration - uint256 duration = _duration.min(maxLockDuration); - // Enforce min lockup duration to prevent flash loan or MEV transaction ordering - duration = duration.max(MIN_LOCK_DURATION); - - depositToken.safeTransferFrom(_msgSender(), address(this), _amount); - - uint256 mintAmount = (_amount * getMultiplier(duration)) / 1e18; - - depositsOf[_receiver].push( - Deposit({ - amount: _amount, - shareAmount: mintAmount, - start: uint64(block.timestamp), - end: uint64(block.timestamp) + uint64(duration) - }) - ); - - _mint(_receiver, mintAmount); - emit Deposited(_amount, mintAmount, duration, _receiver, _msgSender()); - } - - function withdraw(uint256 _depositId, address _receiver) external { - require( - _depositId < depositsOf[_msgSender()].length, - "Deposit does not exist" - ); - Deposit memory userDeposit = depositsOf[_msgSender()][_depositId]; - if (!isAdminUnlock) { - require( - block.timestamp >= userDeposit.end, - "TimeLockPool.withdraw: too soon" - ); - } - - // User must have enough to wthdraw - require( - balanceOf(_msgSender()) >= userDeposit.shareAmount, - "User does not have enough Staking Tokens to withdraw" - ); - // remove Deposit - depositsOf[_msgSender()][_depositId] = depositsOf[_msgSender()][ - depositsOf[_msgSender()].length - 1 - ]; - depositsOf[_msgSender()].pop(); - - // burn pool shares - _burn(_msgSender(), userDeposit.shareAmount); - - // return tokens - depositToken.safeTransfer(_receiver, userDeposit.amount); - emit Withdrawn(_depositId, _receiver, _msgSender(), userDeposit.amount); - } - - function getMultiplier( - uint256 _lockDuration - ) public view returns (uint256) { - uint n = _lockDuration / unit; - if (n == curve.length - 1) { - return 1e18 + curve[n]; - } - return - 1e18 + - curve[n] + - ((_lockDuration - n * unit) * (curve[n + 1] - curve[n])) / - unit; - } - - function getTotalDeposit(address _account) public view returns (uint256) { - uint256 total; - for (uint256 i = 0; i < depositsOf[_account].length; i++) { - total += depositsOf[_account][i].amount; - } - - return total; - } - - function getDepositsOf( - address _account - ) public view returns (Deposit[] memory) { - return depositsOf[_account]; - } - - function getDepositsOfLength( - address _account - ) public view returns (uint256) { - return depositsOf[_account].length; - } - - function adjustPositionStaking() external onlyGov { - isAllowPositionStaking = !isAllowPositionStaking; - } - - function adjustDeposits() external onlyGov { - isAllowDeposits = !isAllowDeposits; - } - - function adjustAdminUnlock() external onlyGov { - isAdminUnlock = !isAdminUnlock; - } - - function adjustMaxBonus(uint256 _maxBonus) external onlyGov { - if (curve.length >= 2) { - require(_maxBonus >= curve[curve.length - 1]); - } - maxBonus = _maxBonus; - } - - function adjustMaxLockPeriod(uint256 _maxLockDuration) external onlyGov { - maxLockDuration = _maxLockDuration; - unit = _maxLockDuration / (curve.length - 1); - } - - function bankersRoundedDiv( - uint256 a, - uint256 b - ) internal pure returns (uint256) { - require(b > 0, "div by 0"); - - uint256 halfB = 0; - if ((b % 2) == 1) { - halfB = (b / 2) + 1; - } else { - halfB = b / 2; - } - bool roundUp = ((a % b) >= halfB); - - // now check if we are in the center! - bool isCenter = ((a % b) == (b / 2)); - bool isDownEven = (((a / b) % 2) == 0); - - // select the rounding type - if (isCenter) { - // only in this case we rounding either DOWN or UP - // depending on what number is even - roundUp = !isDownEven; - } - - // round - if (roundUp) { - return ((a / b) + 1); - } else { - return (a / b); - } - } - - function maxBonusError(uint256 _point) internal view returns (uint256) { - if (_point > maxBonus) { - revert MaxBonusError(); - } else { - return _point; - } - } - - /** - * @notice Can set an entire new curve. - * @dev This function can change current curve by a completely new one. By doing so, it does not - * matter if the new curve's length is larger, equal, or shorter because the function manages - * all of those cases. - * @param _curve uint256 array of the points that compose the curve. - */ - function setCurve(uint256[] calldata _curve) external onlyGov { - // same length curves - if (curve.length == _curve.length) { - for (uint i = 0; i < curve.length; i++) { - curve[i] = maxBonusError(_curve[i]); - } - // replacing with a shorter curve - } else if (curve.length > _curve.length) { - for (uint i = 0; i < _curve.length; i++) { - curve[i] = maxBonusError(_curve[i]); - } - uint initialLength = curve.length; - for (uint j = 0; j < initialLength - _curve.length; j++) { - curve.pop(); - } - unit = maxLockDuration / (curve.length - 1); - // replacing with a longer curve - } else { - for (uint i = 0; i < curve.length; i++) { - curve[i] = maxBonusError(_curve[i]); - } - uint initialLength = curve.length; - for (uint j = 0; j < _curve.length - initialLength; j++) { - curve.push(maxBonusError(_curve[initialLength + j])); - } - unit = maxLockDuration / (curve.length - 1); - } - checkCurve(curve); - emit CurveChanged(_msgSender()); - } - - /** - * @notice Can set a point of the curve. - * @dev This function can replace any point in the curve by inputing the existing index, - * add a point to the curve by using the index that equals the amount of points of the curve, - * and remove the last point of the curve if an index greater than the length is used. The first - * point of the curve index is zero. - * @param _newPoint uint256 point to be set. - * @param _position uint256 position of the array to be set (zero-based indexing convention). - */ - function setCurvePoint( - uint256 _newPoint, - uint256 _position - ) external onlyGov { - if (_newPoint > maxBonus) { - revert MaxBonusError(); - } - if (_position < curve.length) { - curve[_position] = _newPoint; - } else if (_position == curve.length) { - curve.push(_newPoint); - unit = maxLockDuration / (curve.length - 1); - } else { - if (curve.length - 1 < 2) { - revert ShortCurveError(); - } - curve.pop(); - unit = maxLockDuration / (curve.length - 1); - } - checkCurve(curve); - emit CurveChanged(_msgSender()); - } - - function checkCurve(uint256[] memory _curve) internal pure { - if (_curve.length < 2) { - revert ShortCurveError(); - } - for (uint256 i; i < _curve.length - 1; ++i) { - if (_curve[i + 1] < _curve[i]) { - revert CurveIncreaseError(); - } - } - } -} diff --git a/contracts/timelock/base/BaseToken.sol b/contracts/timelock/base/BaseToken.sol index 66ed562..e774b5c 100644 --- a/contracts/timelock/base/BaseToken.sol +++ b/contracts/timelock/base/BaseToken.sol @@ -3,66 +3,44 @@ pragma solidity 0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../../libs/TokenSaver.sol"; -abstract contract BaseToken is - ERC20Permit, - ERC20Votes, - AccessControl, - TokenSaver -{ +abstract contract BaseToken is ERC20, AccessControl, TokenSaver { using SafeERC20 for IERC20; using SafeCast for uint256; using SafeCast for int256; - IERC20 public immutable depositToken; + error NonTransferableError(); - error NotGovError(); - - bytes32 public constant GOV_ROLE = keccak256("GOV_ROLE"); - - modifier onlyGov() { - _onlyGov(); - _; + constructor( + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) { + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } - function _onlyGov() private view { - if (!hasRole(GOV_ROLE, _msgSender())) { - revert NotGovError(); - } + function transfer(address, uint256) public pure override returns (bool) { + revert NonTransferableError(); } - constructor( - string memory _name, - string memory _symbol, - address _depositToken - ) ERC20Permit(_name) ERC20(_name, _symbol) { - require( - _depositToken != address(0), - "BasePool.constructor: Deposit token must be set" - ); - depositToken = IERC20(_depositToken); - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + function transferFrom( + address, + address, + uint256 + ) public pure override returns (bool) { + revert NonTransferableError(); } // The following functions are overrides required by Solidity. - function _update( address from, address to, uint256 value - ) internal override(ERC20, ERC20Votes) { + ) internal override(ERC20) { super._update(from, to, value); } - - function nonces( - address owner - ) public view override(ERC20Permit, Nonces) returns (uint256) { - return super.nonces(owner); - } } diff --git a/contracts/timelock/interfaces/ITimeLockDeposits.sol b/contracts/timelock/interfaces/ITimeLockDeposits.sol index 6b82536..5b60516 100644 --- a/contracts/timelock/interfaces/ITimeLockDeposits.sol +++ b/contracts/timelock/interfaces/ITimeLockDeposits.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.20; interface ITimeLockDeposits { - function deposit(uint256 _amount, uint256 _duration, address _receiver) external; + function deposit(uint256 _amount, uint256 _duration, address _receiver, address _token) external; } \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index 2ccc4f4..f19f850 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -84,8 +84,5 @@ module.exports = { url: "https://rpc.ankr.com/eth_goerli", accounts: [process.env.PRIVATE_KEY], }, - }, - sourcify: { - enabled: true, - }, + } }; diff --git a/scripts/arguments/lockArguments.js b/scripts/arguments/lockArguments.js new file mode 100644 index 0000000..999161a --- /dev/null +++ b/scripts/arguments/lockArguments.js @@ -0,0 +1 @@ +module.exports = ["xLP", "xLP", process.env.LP_TREASURY]; diff --git a/scripts/arguments/stakingArguments.js b/scripts/arguments/stakingArguments.js deleted file mode 100644 index 1fc71ea..0000000 --- a/scripts/arguments/stakingArguments.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = [ - "xVIRTUAL", - "xVIRTUAL", - process.env.BRIDGED_TOKEN, - "16000000000000000000", // maxBonus - 126144000, // maxLockDuration - [ - "0", - "1000000000000000000", - "3000000000000000000", - "7000000000000000000", - "15000000000000000000", - ], // curve -]; diff --git a/scripts/deployTimeLock.ts b/scripts/deployTimeLock.ts index d80d418..006dfe0 100644 --- a/scripts/deployTimeLock.ts +++ b/scripts/deployTimeLock.ts @@ -1,14 +1,15 @@ import { ethers } from "hardhat"; -const deployArguments = require("./arguments/stakingArguments"); +const deployArguments = require("./arguments/lockArguments"); (async () => { try { const contract = await ethers.deployContract( - "TimeLockStaking", + "TimeLock", deployArguments ); await contract.waitForDeployment(); - await contract.grantRole(await contract.GOV_ROLE(), process.env.ADMIN) + await contract.grantRole(await contract.GOV_ROLE(), process.env.DAO) + await contract.grantRole(await contract.ADMIN_ROLE(), process.env.ADMIN) await contract.grantRole(await contract.DEFAULT_ADMIN_ROLE(), process.env.ADMIN) await contract.renounceRole(await contract.DEFAULT_ADMIN_ROLE(), process.env.DEPLOYER) From 1042793b2f7b101d5a571f1b3f8775d8c9885b73 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 15 May 2024 19:13:42 +0800 Subject: [PATCH 02/30] created minter contract --- contracts/contribution/ContributionNft.sol | 1 - contracts/contribution/IContributionNft.sol | 3 + contracts/token/Minter.sol | 108 ++++++++++++++++++++ contracts/virtualPersona/AgentToken.sol | 4 + contracts/virtualPersona/IAgentToken.sol | 2 + 5 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 contracts/token/Minter.sol diff --git a/contracts/contribution/ContributionNft.sol b/contracts/contribution/ContributionNft.sol index 7e799e1..78f2089 100644 --- a/contracts/contribution/ContributionNft.sol +++ b/contracts/contribution/ContributionNft.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "@openzeppelin/contracts/governance/IGovernor.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; diff --git a/contracts/contribution/IContributionNft.sol b/contracts/contribution/IContributionNft.sol index 1612a14..635d43f 100644 --- a/contracts/contribution/IContributionNft.sol +++ b/contracts/contribution/IContributionNft.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import "@openzeppelin/contracts/governance/IGovernor.sol"; + interface IContributionNft { function tokenVirtualId(uint256 tokenId) external view returns (uint256); function tokenURI(uint256 tokenId) external view returns (string memory); @@ -10,4 +12,5 @@ interface IContributionNft { function isModel(uint256 tokenId) external view returns (bool); function getAdmin() external view returns (address); function getDatasetId(uint256 tokenId) external view returns (uint256); + function getAgentDAO(uint256 virtualId) external view returns (IGovernor); } diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol new file mode 100644 index 0000000..70ebcc9 --- /dev/null +++ b/contracts/token/Minter.sol @@ -0,0 +1,108 @@ +pragma solidity ^0.8.20; + +// SPDX-License-Identifier: MIT + +import "../contribution/IServiceNft.sol"; +import "../contribution/IContributionNft.sol"; +import "../virtualPersona/IAgentNft.sol"; +import "../virtualPersona/IAgentToken.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Minter is Ownable { + address public serviceNft; + address public contributionNft; + address public agentNft; + address public ipVault; + + uint256 public ipShare; // Share for IP holder + uint256 public dataShare; // Share for Dataset provider + + mapping(uint256 => bool) _minted; + + bool internal locked; + + constructor( + address serviceAddress, + address contributionAddress, + address agentAddress, + uint256 _ipShare, + address _ipVault, + address initialOwner + ) Ownable(initialOwner) { + serviceNft = serviceAddress; + contributionNft = contributionAddress; + agentNft = agentAddress; + ipShare = _ipShare; + ipVault = _ipVault; + } + + function setServiceNft(address serviceAddress) public onlyOwner { + serviceNft = serviceAddress; + } + + function setContributionNft(address contributionAddress) public onlyOwner { + contributionNft = contributionAddress; + } + + function setIpShare(uint256 _ipShare) public onlyOwner { + ipShare = _ipShare; + } + + function setDataShare(uint256 _dataShare) public onlyOwner { + dataShare = _dataShare; + } + + function setIPVault(address _ipVault) public onlyOwner { + ipVault = _ipVault; + } + + function mint(uint256 nftId) public { + // Mint configuration: + // 1. ELO impact amount, to be shared between model and dataset owner + // 2. IP share amount, ontop of the ELO impact + + require(!_minted[nftId], "Already minted"); + + uint256 agentId = IContributionNft(contributionNft).tokenVirtualId( + nftId + ); + require(agentId != 0, "Agent not found"); + + address dao = address(IContributionNft(contributionNft).getAgentDAO(agentId)); + require(_msgSender() == dao, "Caller is not Agent DAO"); + + _minted[nftId] = true; + + address tokenAddress = IAgentNft(agentNft).virtualInfo(agentId).token; + uint256 datasetId = IContributionNft(contributionNft).getDatasetId( + nftId + ); + uint256 amount = IServiceNft(serviceNft).getImpact(nftId); + uint256 ipAmount = (amount * ipShare) / 10000; + uint256 dataAmount = 0; + + if (datasetId != 0) { + dataAmount = (amount * ipShare) / 10000; + amount = amount - dataAmount; + } + + // Mint to model owner + if (amount > 0) { + address modelOwner = IERC721(contributionNft).ownerOf(nftId); + IAgentToken(tokenAddress).mint(modelOwner, amount); + } + + // Mint to Dataset owner + if (datasetId != 0 && dataAmount > 0) { + address datasetOwner = IERC721(contributionNft).ownerOf(datasetId); + IAgentToken(tokenAddress).mint(datasetOwner, dataAmount); + } + + // To IP vault + if (ipAmount > 0) { + IAgentToken(tokenAddress).mint(ipVault, ipAmount); + } + } +} diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol index f4a7a48..2b74192 100644 --- a/contracts/virtualPersona/AgentToken.sol +++ b/contracts/virtualPersona/AgentToken.sol @@ -146,4 +146,8 @@ contract AgentToken is IAgentToken, ERC20Upgradeable, ERC20Votes { ) public view returns (address) { return super._getPastDelegates(account, timepoint); } + + function mint(address receiver, uint256 amount) external{ + return _mint(receiver, amount); + } } diff --git a/contracts/virtualPersona/IAgentToken.sol b/contracts/virtualPersona/IAgentToken.sol index 1eedcae..88bdb94 100644 --- a/contracts/virtualPersona/IAgentToken.sol +++ b/contracts/virtualPersona/IAgentToken.sol @@ -28,4 +28,6 @@ interface IAgentToken { address account, uint256 timepoint ) external view returns (uint256); + + function mint(address receiver, uint256 amount) external; } From 5b8a9236f013f80a7dc32d682065204c24fabc09 Mon Sep 17 00:00:00 2001 From: kw Date: Mon, 20 May 2024 18:28:39 +0800 Subject: [PATCH 03/30] fixed all build errors --- .../{AgentReward.sol => AgentReward.sol.old} | 0 ...gentRewardV2.sol => AgentRewardV2.sol.old} | 0 ...ardTreasury.sol => RewardTreasury.sol.old} | 0 contracts/governance/AgentVotes.sol | 105 ++++ contracts/governance/ERC1155V.sol | 570 ++++++++++++++++++ .../governance/GovernorVotesUpgradeable.sol | 104 ++++ contracts/governance/IERC1155V.sol | 105 ++++ contracts/governance/IERC1155Votes.sol | 8 + contracts/pool/IUniswapV2Factory.sol | 17 + contracts/pool/IUniswapV2Pair.sol | 52 ++ contracts/pool/IUniswapV2Router01.sol | 95 +++ contracts/pool/IUniswapV2Router02.sol | 44 ++ contracts/token/IMinter.sol | 8 + contracts/token/Minter.sol | 27 +- contracts/virtualPersona/AgentDAO.sol | 30 +- contracts/virtualPersona/AgentFactory.sol | 125 ++-- contracts/virtualPersona/AgentNft.sol | 7 +- contracts/virtualPersona/AgentToken.sol | 141 +---- contracts/virtualPersona/IAgentDAO.sol | 5 +- contracts/virtualPersona/IAgentNft.sol | 6 +- contracts/virtualPersona/IAgentToken.sol | 22 +- 21 files changed, 1252 insertions(+), 219 deletions(-) rename contracts/{AgentReward.sol => AgentReward.sol.old} (100%) rename contracts/{AgentRewardV2.sol => AgentRewardV2.sol.old} (100%) rename contracts/{RewardTreasury.sol => RewardTreasury.sol.old} (100%) create mode 100644 contracts/governance/AgentVotes.sol create mode 100644 contracts/governance/ERC1155V.sol create mode 100644 contracts/governance/GovernorVotesUpgradeable.sol create mode 100644 contracts/governance/IERC1155V.sol create mode 100644 contracts/governance/IERC1155Votes.sol create mode 100644 contracts/pool/IUniswapV2Factory.sol create mode 100644 contracts/pool/IUniswapV2Pair.sol create mode 100644 contracts/pool/IUniswapV2Router01.sol create mode 100644 contracts/pool/IUniswapV2Router02.sol create mode 100644 contracts/token/IMinter.sol diff --git a/contracts/AgentReward.sol b/contracts/AgentReward.sol.old similarity index 100% rename from contracts/AgentReward.sol rename to contracts/AgentReward.sol.old diff --git a/contracts/AgentRewardV2.sol b/contracts/AgentRewardV2.sol.old similarity index 100% rename from contracts/AgentRewardV2.sol rename to contracts/AgentRewardV2.sol.old diff --git a/contracts/RewardTreasury.sol b/contracts/RewardTreasury.sol.old similarity index 100% rename from contracts/RewardTreasury.sol rename to contracts/RewardTreasury.sol.old diff --git a/contracts/governance/AgentVotes.sol b/contracts/governance/AgentVotes.sol new file mode 100644 index 0000000..aa5c280 --- /dev/null +++ b/contracts/governance/AgentVotes.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import {ERC1155V} from "./ERC1155V.sol"; +import "../virtualPersona/IAgentNft.sol"; + +// Voting token for agents +// AgentLP token holders can stake their tokens here in exchange for voting token that they can delegate for voting power +contract AgentVotes is ERC1155V, AccessControl, Initializable { + using SafeERC20 for IERC20; + bool public isAdminUnlock = false; + mapping(address => mapping(uint256 => uint256)) lockedAmounts; + IAgentNft _agentNft; + + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + error Locked(); + + bool internal entranceLocked; + + modifier noReentrant() { + require(!entranceLocked, "cannot reenter"); + entranceLocked = true; + _; + entranceLocked = false; + } + + constructor() { + _disableInitializers(); + } + + function uri(uint256) public pure override returns (string memory) {} + + function initialize(address agentNft, address admin) external initializer { + _agentNft = IAgentNft(agentNft); + _grantRole(ADMIN_ROLE, admin); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + function setAdminUnlock(bool unlock) public { + isAdminUnlock = unlock; + } + + // Stakers have to stake their tokens and delegate to a validator + function stake( + uint256 id, // Agent Id is equivalent to token Id + uint256 amount, + address receiver, + address delegatee, + bool locked, + bytes calldata data + ) public { + address sender = _msgSender(); + require(amount > 0, "Cannot stake 0"); + + if (locked) { + lockedAmounts[receiver][id] += amount; + } + + // Get token from NFT + IERC20(_lpToken(id)).safeTransferFrom(sender, address(this), amount); + + _mint(receiver, id, amount, data); + _delegate(receiver, delegatee, id); + } + + function withdraw(uint256 id, uint256 amount) public noReentrant { + address sender = _msgSender(); + uint256 balance = balanceOf[sender][id]; + require(balance >= amount, "Insufficient balance"); + + uint256 withdrawable = isAdminUnlock + ? balance + : balance - lockedAmounts[sender][id]; + if (withdrawable < amount) { + revert Locked(); + } + + _burn(sender, id, amount); + + IERC20(_lpToken(id)).safeTransfer(sender, amount); + } + + // Get LP token address from Agent NFT + function _lpToken(uint256 id) internal view returns (address) { + address lpToken = _agentNft.virtualInfo(id).pool; + } + + function supportsInterface( + bytes4 interfaceId + ) public view override(AccessControl, ERC1155V) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function clock() external view returns (uint48) { + return uint48(block.number); + } + + function CLOCK_MODE() external view returns (string memory) { + return "mode=blocknumber&from=default"; + } +} diff --git a/contracts/governance/ERC1155V.sol b/contracts/governance/ERC1155V.sol new file mode 100644 index 0000000..bab0626 --- /dev/null +++ b/contracts/governance/ERC1155V.sol @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./IERC1155Votes.sol"; +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; + +/// @notice A generic interface for a contract which properly accepts ERC1155 tokens. +/// @author Modified from ERC1155V (https://github.com/kalidao/ERC1155V/blob/main/src/ERC1155V.sol) +abstract contract ERC1155TokenReceiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external payable virtual returns (bytes4) { + return ERC1155TokenReceiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external payable virtual returns (bytes4) { + return ERC1155TokenReceiver.onERC1155BatchReceived.selector; + } +} + +abstract contract ERC1155V is IERC1155Votes { + /// ----------------------------------------------------------------------- + /// Events + /// ----------------------------------------------------------------------- + + event SupplyChanged( + uint256 indexed id, + uint256 previousBalance, + uint256 newBalance + ); + + /// ----------------------------------------------------------------------- + /// Custom Errors + /// ----------------------------------------------------------------------- + + error LengthMismatch(); + + error NotAuthorized(); + + error UnsafeRecipient(); + + error InvalidRecipient(); + + error Undetermined(); + + error Overflow(); + + /// ----------------------------------------------------------------------- + /// ERC1155 Storage + /// ----------------------------------------------------------------------- + + mapping(address => mapping(uint256 => uint256)) public balanceOf; + + mapping(address => mapping(address => bool)) public isApprovedForAll; + + /// ----------------------------------------------------------------------- + /// Voting Storage + /// ----------------------------------------------------------------------- + + mapping(uint256 => uint256) public totalSupply; + + mapping(address => mapping(uint256 => address)) internal _delegates; + + mapping(address => mapping(uint256 => uint256)) public numCheckpoints; + + mapping(address => mapping(uint256 => mapping(uint256 => Checkpoint))) + public checkpoints; + + mapping(uint256 => uint256) public numSupplyCheckpoints; + + mapping(uint256 => mapping(uint256 => Checkpoint)) public supplyCheckpoints; + + struct Checkpoint { + uint40 fromTimestamp; + uint216 amount; + } + + /// ----------------------------------------------------------------------- + /// Metadata Logic + /// ----------------------------------------------------------------------- + + function uri(uint256 id) public view virtual returns (string memory); + + /// ----------------------------------------------------------------------- + /// ERC165 Logic + /// ----------------------------------------------------------------------- + + function supportsInterface( + bytes4 interfaceId + ) public view virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. + interfaceId == 0xd9b67a26 || // ERC165 interface ID for ERC1155. + interfaceId == 0x0e89341c; // ERC165 interface ID for ERC1155MetadataURI. + } + + /// ----------------------------------------------------------------------- + /// ERC1155 Logic + /// ----------------------------------------------------------------------- + + function balanceOfBatch( + address[] calldata owners, + uint256[] calldata ids + ) public view virtual returns (uint256[] memory balances) { + if (owners.length != ids.length) revert LengthMismatch(); + + balances = new uint256[](owners.length); + + for (uint256 i; i < owners.length; ) { + balances[i] = balanceOf[owners[i]][ids[i]]; + + // Unchecked because the only math done is incrementing + // the array index counter which cannot possibly overflow. + unchecked { + ++i; + } + } + } + + function setApprovalForAll( + address operator, + bool approved + ) public payable virtual { + isApprovedForAll[msg.sender][operator] = approved; + + emit ApprovalForAll(msg.sender, operator, approved); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) public payable virtual { + if (msg.sender != from && !isApprovedForAll[from][msg.sender]) + revert NotAuthorized(); + + balanceOf[from][id] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to][id] += amount; + } + + emit TransferSingle(msg.sender, from, to, id, amount); + + if (to.code.length != 0) { + if ( + ERC1155TokenReceiver(to).onERC1155Received( + msg.sender, + from, + id, + amount, + data + ) != ERC1155TokenReceiver.onERC1155Received.selector + ) { + revert UnsafeRecipient(); + } + } else { + if (to == address(0)) { + revert InvalidRecipient(); + } + } + + _moveDelegates(delegates(from, id), delegates(to, id), id, amount); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) public payable virtual { + if (ids.length != amounts.length) revert LengthMismatch(); + + if (msg.sender != from && !isApprovedForAll[from][msg.sender]) + revert NotAuthorized(); + + // Storing these outside the loop saves ~15 gas per iteration. + uint256 id; + uint256 amount; + + for (uint256 i; i < ids.length; ) { + id = ids[i]; + amount = amounts[i]; + + balanceOf[from][id] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to][id] += amount; + } + + _moveDelegates(delegates(from, id), delegates(to, id), id, amount); + + // An array can't have a total length + // larger than the max uint256 value. + unchecked { + ++i; + } + } + + emit TransferBatch(msg.sender, from, to, ids, amounts); + + if (to.code.length != 0) { + if ( + ERC1155TokenReceiver(to).onERC1155BatchReceived( + msg.sender, + from, + ids, + amounts, + data + ) != ERC1155TokenReceiver.onERC1155BatchReceived.selector + ) { + revert UnsafeRecipient(); + } + } else if (to == address(0)) { + revert InvalidRecipient(); + } + } + + /// ----------------------------------------------------------------------- + /// Voting Logic + /// ----------------------------------------------------------------------- + + function delegates( + address account, + uint256 id + ) public view virtual returns (address) { + address current = _delegates[account][id]; + + return current == address(0) ? account : current; + } + + function getVotes( + address account, + uint256 id + ) public view virtual returns (uint256) { + // Won't underflow because decrement only occurs if positive `nCheckpoints`. + unchecked { + uint256 nCheckpoints = numCheckpoints[account][id]; + + return + nCheckpoints != 0 + ? checkpoints[account][id][nCheckpoints - 1].amount + : 0; + } + } + + function getPastVotes( + address account, + uint256 id, + uint256 timestamp + ) public view virtual returns (uint256) { + if (block.timestamp <= timestamp) revert Undetermined(); + + uint256 nCheckpoints = numCheckpoints[account][id]; + + if (nCheckpoints == 0) return 0; + + // Won't underflow because decrement only occurs if positive `nCheckpoints`. + unchecked { + uint256 prevCheckpoint = nCheckpoints - 1; + + if ( + checkpoints[account][id][prevCheckpoint].fromTimestamp <= + timestamp + ) return checkpoints[account][id][prevCheckpoint].amount; + + if (checkpoints[account][id][0].fromTimestamp > timestamp) return 0; + + uint256 lower; + + uint256 upper = prevCheckpoint; + + while (upper > lower) { + uint256 center = upper - (upper - lower) / 2; + + Checkpoint memory cp = checkpoints[account][id][center]; + + if (cp.fromTimestamp == timestamp) { + return cp.amount; + } else if (cp.fromTimestamp < timestamp) { + lower = center; + } else { + upper = center - 1; + } + } + + return checkpoints[account][id][lower].amount; + } + } + + function getPastTotalSupply( + uint256 id, + uint256 timestamp + ) public view returns (uint256) { + if (block.timestamp <= timestamp) revert Undetermined(); + + uint256 nCheckpoints = numSupplyCheckpoints[id]; + + if (nCheckpoints == 0) return 0; + + // Won't underflow because decrement only occurs if positive `nCheckpoints`. + unchecked { + uint256 prevCheckpoint = nCheckpoints - 1; + + if ( + supplyCheckpoints[id][prevCheckpoint].fromTimestamp <= timestamp + ) return supplyCheckpoints[id][prevCheckpoint].amount; + + if (supplyCheckpoints[id][0].fromTimestamp > timestamp) return 0; + + uint256 lower; + + uint256 upper = prevCheckpoint; + + while (upper > lower) { + uint256 center = upper - (upper - lower) / 2; + + Checkpoint memory cp = supplyCheckpoints[id][center]; + + if (cp.fromTimestamp == timestamp) { + return cp.amount; + } else if (cp.fromTimestamp < timestamp) { + lower = center; + } else { + upper = center - 1; + } + } + + return supplyCheckpoints[id][lower].amount; + } + } + + function delegate(address delegatee, uint256 id) public virtual { + _delegate(msg.sender, delegatee, id); + } + + function _delegate(address delegator, address delegatee, uint256 id) internal { + address currentDelegate = delegates(delegator, id); + + _delegates[delegator][id] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee, id); + + _moveDelegates( + currentDelegate, + delegatee, + id, + balanceOf[delegator][id] + ); + } + + function _moveDelegates( + address srcRep, + address dstRep, + uint256 id, + uint256 amount + ) internal virtual { + if (srcRep != dstRep && amount != 0) { + if (srcRep != address(0)) { + uint256 srcRepNum = numCheckpoints[srcRep][id]; + + uint256 srcRepOld; + + // Won't underflow because decrement only occurs if positive `srcRepNum`. + unchecked { + srcRepOld = srcRepNum != 0 + ? checkpoints[srcRep][id][srcRepNum - 1].amount + : 0; + } + + _writeCheckpoint( + srcRep, + id, + srcRepNum, + srcRepOld, + srcRepOld - amount + ); + } + + if (dstRep != address(0)) { + uint256 dstRepNum = numCheckpoints[dstRep][id]; + + uint256 dstRepOld; + + // Won't underflow because decrement only occurs if positive `dstRepNum`. + unchecked { + dstRepOld = dstRepNum != 0 + ? checkpoints[dstRep][id][dstRepNum - 1].amount + : 0; + } + + _writeCheckpoint( + dstRep, + id, + dstRepNum, + dstRepOld, + dstRepOld + amount + ); + } + } + } + + function _writeSupplyCheckpoint( + uint256 id, + uint256 nCheckpoints, + uint256 oldAmount, + uint256 newAmount + ) internal virtual { + // Won't underflow because decrement only occurs if positive `nCheckpoints`. + unchecked { + if ( + nCheckpoints != 0 && + supplyCheckpoints[id][nCheckpoints - 1].fromTimestamp == + block.timestamp + ) { + supplyCheckpoints[id][nCheckpoints - 1].amount = _safeCastTo216( + newAmount + ); + } else { + supplyCheckpoints[id][nCheckpoints] = Checkpoint( + _safeCastTo40(block.timestamp), + _safeCastTo216(newAmount) + ); + + // Won't realistically overflow. + ++numSupplyCheckpoints[id]; + } + } + + emit SupplyChanged(id, oldAmount, newAmount); + } + + function _writeCheckpoint( + address delegatee, + uint256 id, + uint256 nCheckpoints, + uint256 oldVotes, + uint256 newVotes + ) internal virtual { + // Won't underflow because decrement only occurs if positive `nCheckpoints`. + unchecked { + if ( + nCheckpoints != 0 && + checkpoints[delegatee][id][nCheckpoints - 1].fromTimestamp == + block.timestamp + ) { + checkpoints[delegatee][id][nCheckpoints - 1] + .amount = _safeCastTo216(newVotes); + } else { + checkpoints[delegatee][id][nCheckpoints] = Checkpoint( + _safeCastTo40(block.timestamp), + _safeCastTo216(newVotes) + ); + + // Won't realistically overflow. + ++numCheckpoints[delegatee][id]; + } + } + + emit DelegateVotesChanged(delegatee, id, oldVotes, newVotes); + } + + function _safeCastTo40(uint256 x) internal pure virtual returns (uint40 y) { + if (x >= (1 << 40)) revert Overflow(); + + y = uint40(x); + } + + function _safeCastTo216( + uint256 x + ) internal pure virtual returns (uint216 y) { + if (x >= (1 << 216)) revert Overflow(); + + y = uint216(x); + } + + /// ----------------------------------------------------------------------- + /// Internal ID Logic + /// ----------------------------------------------------------------------- + + function _mint( + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) internal virtual { + _safeCastTo216(totalSupply[id] += amount); + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to][id] += amount; + } + + emit TransferSingle(msg.sender, address(0), to, id, amount); + + if (to.code.length != 0) { + if ( + ERC1155TokenReceiver(to).onERC1155Received( + msg.sender, + address(0), + id, + amount, + data + ) != ERC1155TokenReceiver.onERC1155Received.selector + ) { + revert UnsafeRecipient(); + } + } else if (to == address(0)) { + revert InvalidRecipient(); + } + + _moveDelegates(address(0), delegates(to, id), id, amount); + + uint256 nCheckpoints = numSupplyCheckpoints[id]; + + uint256 oldAmount; + + // Won't underflow because decrement only occurs if positive `nCheckpoints`. + unchecked { + oldAmount = nCheckpoints != 0 + ? supplyCheckpoints[id][nCheckpoints - 1].amount + : 0; + } + _writeSupplyCheckpoint(id, nCheckpoints, oldAmount, oldAmount + amount); + } + + function _burn(address from, uint256 id, uint256 amount) internal virtual { + balanceOf[from][id] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply[id] -= amount; + } + + emit TransferSingle(msg.sender, from, address(0), id, amount); + + _moveDelegates(delegates(from, id), address(0), id, amount); + + uint256 nCheckpoints = numSupplyCheckpoints[id]; + + uint256 oldAmount; + + // Won't underflow because decrement only occurs if positive `nCheckpoints`. + unchecked { + oldAmount = nCheckpoints != 0 + ? supplyCheckpoints[id][nCheckpoints - 1].amount + : 0; + } + _writeSupplyCheckpoint(id, nCheckpoints, oldAmount, oldAmount - amount); + } +} diff --git a/contracts/governance/GovernorVotesUpgradeable.sol b/contracts/governance/GovernorVotesUpgradeable.sol new file mode 100644 index 0000000..29f6595 --- /dev/null +++ b/contracts/governance/GovernorVotesUpgradeable.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorVotes.sol) + +pragma solidity ^0.8.20; + +import {GovernorUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; +import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol"; +import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "./IERC1155Votes.sol"; +import {IERC6372} from "@openzeppelin/contracts/interfaces/IERC6372.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} + * token. + */ +abstract contract GovernorVotesUpgradeable is + Initializable, + GovernorUpgradeable +{ + uint256 public tokenId; + + /// @custom:storage-location erc7201:openzeppelin.storage.GovernorVotes + struct GovernorVotesStorage { + IERC1155Votes _token; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.GovernorVotes")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant GovernorVotesStorageLocation = + 0x3ba4977254e415696610a40ebf2258dbfa0ec6a2ff64e84bfe715ff16977cc00; + + function _getGovernorVotesStorage() + private + pure + returns (GovernorVotesStorage storage $) + { + assembly { + $.slot := GovernorVotesStorageLocation + } + } + + function __GovernorVotes_init( + IERC1155Votes tokenAddress, + uint256 _tokenId + ) internal onlyInitializing { + __GovernorVotes_init_unchained(tokenAddress, _tokenId); + } + + function __GovernorVotes_init_unchained( + IERC1155Votes tokenAddress, + uint256 _tokenId + ) internal onlyInitializing { + GovernorVotesStorage storage $ = _getGovernorVotesStorage(); + $._token = IERC1155Votes(address(tokenAddress)); + tokenId = _tokenId; + } + + /** + * @dev The token that voting power is sourced from. + */ + function token() public view virtual returns (IERC1155Votes) { + GovernorVotesStorage storage $ = _getGovernorVotesStorage(); + return $._token; + } + + /** + * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token + * does not implement EIP-6372. + */ + function clock() public view virtual override returns (uint48) { + try token().clock() returns (uint48 timepoint) { + return timepoint; + } catch { + return Time.blockNumber(); + } + } + + /** + * @dev Machine-readable description of the clock as specified in EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + try token().CLOCK_MODE() returns ( + string memory clockmode + ) { + return clockmode; + } catch { + return "mode=blocknumber&from=default"; + } + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}). + */ + function _getVotes( + address account, + uint256 timepoint, + bytes memory /*params*/ + ) internal view virtual override returns (uint256) { + return token().getPastVotes(account, tokenId, timepoint); + } +} diff --git a/contracts/governance/IERC1155V.sol b/contracts/governance/IERC1155V.sol new file mode 100644 index 0000000..e58151c --- /dev/null +++ b/contracts/governance/IERC1155V.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +// Modified from OpenZeppelin Contracts (last updated v5.0.0) (governance/utils/IVotes.sol) +pragma solidity ^0.8.20; + +interface IERC1155V { + + error VotesExpiredSignature(uint256 expiry); + + event DelegateChanged( + address indexed delegator, + address indexed fromDelegate, + address indexed toDelegate, + uint256 id + ); + + event DelegateVotesChanged( + address indexed delegate, + uint256 indexed id, + uint256 previousBalance, + uint256 newBalance + ); + + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 amount + ); + + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] amounts + ); + + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + + event TransferabilitySet( + address indexed operator, + uint256 indexed id, + bool set + ); + + event URI(string value, uint256 indexed id); + + /** + * @dev Returns the current amount of votes that `account` has. + */ + + function getVotes(address account, uint256 id) external view returns (uint256); + + /** + * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. + */ + function getPastVotes( + address account, + uint256 id, + uint256 timepoint + ) external view returns (uint256); + + /** + * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. + * + * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. + * Votes that have not been delegated are still part of total supply, even though they would not participate in a + * vote. + */ + function getPastTotalSupply( + uint256 id, + uint256 timepoint + ) external view returns (uint256); + + /** + * @dev Returns the delegate that `account` has chosen. + */ + function delegates(address account, uint256 id) external view returns (address); + + /** + * @dev Delegates votes from the sender to `delegatee`. + */ + function delegate(address delegatee, uint256 id) external; + + // TODO + // /** + // * @dev Delegates votes from signer to `delegatee`. + // */ + // function delegateBySig( + // address delegatee, + // uint256 id, + // uint256 nonce, + // uint256 expiry, + // uint8 v, + // bytes32 r, + // bytes32 s + // ) external; +} diff --git a/contracts/governance/IERC1155Votes.sol b/contracts/governance/IERC1155Votes.sol new file mode 100644 index 0000000..2e76f29 --- /dev/null +++ b/contracts/governance/IERC1155Votes.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +// Modified from OpenZeppelin Contracts (last updated v5.0.0) (governance/utils/IVotes.sol) +pragma solidity ^0.8.20; + +import "./IERC1155V.sol"; +import {IERC6372} from "@openzeppelin/contracts/interfaces/IERC6372.sol"; + +interface IERC1155Votes is IERC1155V, IERC6372 {} diff --git a/contracts/pool/IUniswapV2Factory.sol b/contracts/pool/IUniswapV2Factory.sol new file mode 100644 index 0000000..269974a --- /dev/null +++ b/contracts/pool/IUniswapV2Factory.sol @@ -0,0 +1,17 @@ +pragma solidity >=0.5.0; + +interface IUniswapV2Factory { + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint) external view returns (address pair); + function allPairsLength() external view returns (uint); + + function createPair(address tokenA, address tokenB) external returns (address pair); + + function setFeeTo(address) external; + function setFeeToSetter(address) external; +} \ No newline at end of file diff --git a/contracts/pool/IUniswapV2Pair.sol b/contracts/pool/IUniswapV2Pair.sol new file mode 100644 index 0000000..89f1c61 --- /dev/null +++ b/contracts/pool/IUniswapV2Pair.sol @@ -0,0 +1,52 @@ +pragma solidity >=0.5.0; + +interface IUniswapV2Pair { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); + function kLast() external view returns (uint); + + function mint(address to) external returns (uint liquidity); + function burn(address to) external returns (uint amount0, uint amount1); + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} \ No newline at end of file diff --git a/contracts/pool/IUniswapV2Router01.sol b/contracts/pool/IUniswapV2Router01.sol new file mode 100644 index 0000000..5ee973e --- /dev/null +++ b/contracts/pool/IUniswapV2Router01.sol @@ -0,0 +1,95 @@ +pragma solidity >=0.6.2; + +interface IUniswapV2Router01 { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external payable returns (uint amountToken, uint amountETH, uint liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountToken, uint amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountA, uint amountB); + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountToken, uint amountETH); + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} \ No newline at end of file diff --git a/contracts/pool/IUniswapV2Router02.sol b/contracts/pool/IUniswapV2Router02.sol new file mode 100644 index 0000000..8932ea9 --- /dev/null +++ b/contracts/pool/IUniswapV2Router02.sol @@ -0,0 +1,44 @@ +pragma solidity >=0.6.2; + +import "./IUniswapV2Router01.sol"; + +interface IUniswapV2Router02 is IUniswapV2Router01 { + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountETH); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external payable; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; +} \ No newline at end of file diff --git a/contracts/token/IMinter.sol b/contracts/token/IMinter.sol new file mode 100644 index 0000000..8233044 --- /dev/null +++ b/contracts/token/IMinter.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IMinter { + function mintInitial(address token) external; + + function mint(uint256 nftId) external; +} diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol index 70ebcc9..072e27e 100644 --- a/contracts/token/Minter.sol +++ b/contracts/token/Minter.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; // SPDX-License-Identifier: MIT +import "./IMinter.sol"; import "../contribution/IServiceNft.sol"; import "../contribution/IContributionNft.sol"; import "../virtualPersona/IAgentNft.sol"; @@ -10,7 +11,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Minter is Ownable { +contract Minter is IMinter, Ownable { address public serviceNft; address public contributionNft; address public agentNft; @@ -19,16 +20,21 @@ contract Minter is Ownable { uint256 public ipShare; // Share for IP holder uint256 public dataShare; // Share for Dataset provider + uint256 public bootstrapAmount; // Amount for initial LP bootstrap + mapping(uint256 => bool) _minted; bool internal locked; + address agentFactory; + constructor( address serviceAddress, address contributionAddress, address agentAddress, uint256 _ipShare, address _ipVault, + address _agentFactory, address initialOwner ) Ownable(initialOwner) { serviceNft = serviceAddress; @@ -36,6 +42,13 @@ contract Minter is Ownable { agentNft = agentAddress; ipShare = _ipShare; ipVault = _ipVault; + agentFactory = _agentFactory; + } + + modifier onlyFactory() { + require(_msgSender() == agentFactory, "Caller is not Agent Factory"); + _; + } function setServiceNft(address serviceAddress) public onlyOwner { @@ -58,6 +71,18 @@ contract Minter is Ownable { ipVault = _ipVault; } + function setAgentFactory(address _factory) public onlyOwner { + agentFactory = _factory; + } + + function setBootstrapAmount(uint256 amount) public onlyOwner { + bootstrapAmount = amount; + } + + function mintInitial(address token) public onlyFactory { + IAgentToken(token).mint(_msgSender(), bootstrapAmount); + } + function mint(uint256 nftId) public { // Mint configuration: // 1. ELO impact amount, to be shared between model and dataset owner diff --git a/contracts/virtualPersona/AgentDAO.sol b/contracts/virtualPersona/AgentDAO.sol index 0726438..f477706 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -1,26 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorStorageUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol"; +//import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol"; import "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; import "./IAgentDAO.sol"; import "./GovernorCountingSimpleUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "../contribution/IContributionNft.sol"; +import "../governance/IERC1155Votes.sol"; +import "../governance/GovernorVotesUpgradeable.sol"; + +//GovernorVotesUpgradeable, +//GovernorVotesQuorumFractionUpgradeable contract AgentDAO is IAgentDAO, - GovernorUpgradeable, GovernorSettingsUpgradeable, GovernorCountingSimpleUpgradeable, GovernorStorageUpgradeable, - GovernorVotesUpgradeable, - GovernorVotesQuorumFractionUpgradeable + GovernorVotesUpgradeable { using Checkpoints for Checkpoints.Trace208; @@ -37,7 +38,8 @@ contract AgentDAO is function initialize( string memory name, - IVotes token, + IERC1155Votes token, + uint256 tokenId_, address contributionNft, uint256 threshold, uint32 votingPeriod_ @@ -45,8 +47,8 @@ contract AgentDAO is __Governor_init(name); __GovernorSettings_init(0, votingPeriod_, threshold); __GovernorCountingSimple_init(); - __GovernorVotes_init(token); - __GovernorVotesQuorumFraction_init(5100); + __GovernorVotes_init(token, tokenId_); + // __GovernorVotesQuorumFraction_init(5100); __GovernorCountingSimple_init(); __GovernorStorage_init(); @@ -215,13 +217,13 @@ contract AgentDAO is ) public view - override(GovernorUpgradeable, GovernorVotesQuorumFractionUpgradeable) + override(GovernorUpgradeable) returns (uint256) { - return super.quorum(blockNumber); + return token().getPastTotalSupply(tokenId, blockNumber); } - function quorumDenominator() public pure override returns (uint256) { - return 10000; - } + // function quorumDenominator() public pure override returns (uint256) { + // return 10000; + // } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index fa9baa5..89947c0 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -13,21 +13,28 @@ import "./IAgentToken.sol"; import "./IAgentDAO.sol"; import "./IAgentNft.sol"; import "../libs/IERC6551Registry.sol"; +import "../pool/IUniswapV2Router02.sol"; +import "../pool/IUniswapV2Factory.sol"; +import "../pool/IUniswapV2Pair.sol"; +import "../governance/IERC1155Votes.sol"; contract AgentFactory is Initializable, AccessControl { using SafeERC20 for IERC20; uint256 private _nextId; + IUniswapV2Router02 internal _uniswapRouter; + address public tokenImplementation; address public daoImplementation; address public nft; address public tbaRegistry; // Token bound account uint256 public applicationThreshold; + IERC1155Votes public voteToken; address[] public allTokens; address[] public allDAOs; - address public assetToken; // Staked token + address public assetToken; // Base currency uint256 public maturityDuration; // Maturity duration in seconds bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE"); @@ -36,7 +43,8 @@ contract AgentFactory is Initializable, AccessControl { uint256 virtualId, address token, address dao, - address tba + address tba, + address lp ); event NewApplication(uint256 id); @@ -64,15 +72,7 @@ contract AgentFactory is Initializable, AccessControl { mapping(uint256 => Application) private _applications; - address public gov; - - modifier onlyGov() { - require(msg.sender == gov, "Only DAO can execute proposal"); - _; - } - event ApplicationThresholdUpdated(uint256 newThreshold); - event GovUpdated(address newGov); event ImplContractsUpdated(address token, address dao); address private _vault; // Vault to hold all Virtual NFTs @@ -86,6 +86,8 @@ contract AgentFactory is Initializable, AccessControl { locked = false; } + address minter; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -99,8 +101,10 @@ contract AgentFactory is Initializable, AccessControl { address nft_, uint256 applicationThreshold_, uint256 maturityDuration_, - address gov_, - address vault_ + address vault_, + address minter_, + address uniswapRouter_, + address voteToken_ ) public initializer { tokenImplementation = tokenImplementation_; daoImplementation = daoImplementation_; @@ -112,8 +116,10 @@ contract AgentFactory is Initializable, AccessControl { _nextId = 1; _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(WITHDRAW_ROLE, msg.sender); - gov = gov_; _vault = vault_; + minter = minter_; + _uniswapRouter = IUniswapV2Router02(uniswapRouter_); + voteToken = IERC1155Votes(voteToken_); } function getApplication( @@ -131,8 +137,8 @@ contract AgentFactory is Initializable, AccessControl { address tbaImplementation, uint32 daoVotingPeriod, uint256 daoThreshold - ) external returns (uint256) { - address sender = msg.sender; + ) public returns (uint256) { + address sender = _msgSender(); require( IERC20(assetToken).balanceOf(sender) >= applicationThreshold, "Insufficient asset token" @@ -151,9 +157,7 @@ contract AgentFactory is Initializable, AccessControl { ); uint256 id = _nextId++; - uint256 proposalEndBlock = block.number + - IGovernor(gov).votingPeriod() + - IGovernor(gov).votingDelay(); + uint256 proposalEndBlock = block.number; // No longer required in v2 Application memory application = Application( name, symbol, @@ -203,41 +207,47 @@ contract AgentFactory is Initializable, AccessControl { ); } - function executeApplication(uint256 id) public onlyGov { + function executeApplication(uint256 id) public noReentrant { require( _applications[id].status == ApplicationStatus.Active, "Application is not active" ); Application storage application = _applications[id]; + + application.withdrawableAmount = 0; + application.status = ApplicationStatus.Executed; + address token = _createNewAgentToken( application.name, - application.symbol, - application.proposer + application.symbol ); + + // Create LP + address lp = _createLP(token, assetToken); + uint256 virtualId = IAgentNft(nft).nextVirtualId(); + string memory daoName = string.concat(application.name, " DAO"); address payable dao = payable( _createNewDAO( daoName, - IVotes(token), + voteToken, + virtualId, application.daoVotingPeriod, application.daoThreshold ) ); - uint256 virtualId = IAgentNft(nft).mint( + + IAgentNft(nft).mint( + virtualId, _vault, application.tokenURI, dao, application.proposer, application.cores ); + application.virtualId = virtualId; - IERC20(assetToken).forceApprove(token, application.withdrawableAmount); - IAgentToken(token).stake( - application.withdrawableAmount, - application.proposer, - application.proposer - ); uint256 chainId; assembly { chainId := chainid() @@ -252,16 +262,41 @@ contract AgentFactory is Initializable, AccessControl { IAgentNft(nft).setTBA(virtualId, tbaAddress); - application.withdrawableAmount = 0; - application.status = ApplicationStatus.Executed; - application.virtualId = virtualId; + // Call minter to mint initial tokens + // Stake in LP + // Add liquidity + IAgentToken(token).mint(address(this), 10 ** 18); + IERC20(token).transfer(lp, 10 ** 18); + IERC20(assetToken).transfer(lp, 10 ** 18); + IUniswapV2Pair(lp).mint(address(this)); + // TODO: Check how much LP token minted + + // IERC20(assetToken).forceApprove(token, application.withdrawableAmount); + // IAgentToken(token).stake( + // application.withdrawableAmount, + // application.proposer, + // application.proposer + // ); + + emit NewPersona(virtualId, token, dao, tbaAddress, lp); + } - emit NewPersona(virtualId, token, dao, tbaAddress); + function _createLP( + address token_, + address assetToken_ + ) internal returns (address uniswapV2Pair) { + uniswapV2Pair = IUniswapV2Factory(_uniswapRouter.factory()).createPair( + token_, + assetToken_ + ); + + return uniswapV2Pair; } function _createNewDAO( string memory name, - IVotes token, + IERC1155Votes token, + uint256 tokenId, uint32 daoVotingPeriod, uint256 daoThreshold ) internal returns (address instance) { @@ -269,6 +304,7 @@ contract AgentFactory is Initializable, AccessControl { IAgentDAO(instance).initialize( name, token, + tokenId, IAgentNft(nft).getContributionNft(), daoThreshold, daoVotingPeriod @@ -280,18 +316,10 @@ contract AgentFactory is Initializable, AccessControl { function _createNewAgentToken( string memory name, - string memory symbol, - address founder + string memory symbol ) internal returns (address instance) { instance = Clones.clone(tokenImplementation); - IAgentToken(instance).initialize( - name, - symbol, - founder, - assetToken, - nft, - block.timestamp + maturityDuration - ); + IAgentToken(instance).initialize(name, symbol, minter); allTokens.push(instance); return instance; @@ -308,11 +336,6 @@ contract AgentFactory is Initializable, AccessControl { emit ApplicationThresholdUpdated(newThreshold); } - function setGov(address newGov) public onlyRole(DEFAULT_ADMIN_ROLE) { - gov = newGov; - emit GovUpdated(newGov); - } - function setVault(address newVault) public onlyRole(DEFAULT_ADMIN_ROLE) { _vault = newVault; } @@ -324,4 +347,8 @@ contract AgentFactory is Initializable, AccessControl { tokenImplementation = token; daoImplementation = dao; } + + function setMinter(address newMinter) public onlyRole(DEFAULT_ADMIN_ROLE) { + minter = newMinter; + } } diff --git a/contracts/virtualPersona/AgentNft.sol b/contracts/virtualPersona/AgentNft.sol index 05f3e04..7fe818a 100644 --- a/contracts/virtualPersona/AgentNft.sol +++ b/contracts/virtualPersona/AgentNft.sol @@ -77,14 +77,19 @@ contract AgentNft is _serviceNft = serviceNft_; } + function nextVirtualId() public view returns (uint256) { + return _nextVirtualId; + } + function mint( + uint256 virtualId, address to, string memory newTokenURI, address payable theDAO, address founder, uint8[] memory coreTypes ) external onlyRole(MINTER_ROLE) returns (uint256) { - uint256 virtualId = _nextVirtualId++; + _nextVirtualId++; _mint(to, virtualId); _setTokenURI(virtualId, newTokenURI); VirtualInfo storage info = virtualInfos[virtualId]; diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol index 2b74192..19cbcb2 100644 --- a/contracts/virtualPersona/AgentToken.sol +++ b/contracts/virtualPersona/AgentToken.sol @@ -4,150 +4,31 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; -import "./IAgentToken.sol"; -import "./IAgentNft.sol"; -import "./ERC20Votes.sol"; - -contract AgentToken is IAgentToken, ERC20Upgradeable, ERC20Votes { - using SafeERC20 for IERC20; - using Checkpoints for Checkpoints.Trace208; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; - address public founder; - address public assetToken; // This is the token that is staked - address public virtualNft; // We can delegate to validator only - uint256 public matureAt; // The timestamp when the founder can withdraw the tokens +import "./IAgentToken.sol"; +contract AgentToken is IAgentToken, AccessControlUpgradeable, ERC20Upgradeable { constructor() { _disableInitializers(); } - modifier canDelegate(address target) { - IAgentNft registry = IAgentNft(virtualNft); - uint256 virtualId = registry.stakingTokenToVirtualId(address(this)); - require( - registry.isValidator(virtualId, target), - "Delegatee is not a validator" - ); - _; - } - - mapping(address => Checkpoints.Trace208) private _balanceCheckpoints; - - bool internal locked; - - modifier noReentrant() { - require(!locked, "cannot reenter"); - locked = true; - _; - locked = false; - } + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); function initialize( string memory name, string memory symbol, - address theFounder, - address theAssetToken, - address theVirtualNft, - uint256 theMatureAt + address minter ) external initializer { __ERC20_init(name, symbol); - __ERC20Votes_init(); - - founder = theFounder; - assetToken = theAssetToken; - virtualNft = theVirtualNft; - matureAt = theMatureAt; - } - - // Stakers have to stake their tokens and delegate to a validator - function stake( - uint256 amount, - address receiver, - address delegatee - ) public canDelegate(delegatee) { - address sender = _msgSender(); - require(amount > 0, "Cannot stake 0"); - - IERC20(assetToken).safeTransferFrom(sender, address(this), amount); - _mint(receiver, amount); - _delegate(receiver, delegatee); - _balanceCheckpoints[receiver].push( - clock(), - SafeCast.toUint208(balanceOf(receiver)) - ); + __AccessControl_init(); + _grantRole(MINTER_ROLE, minter); } - function withdraw(uint256 amount) public noReentrant { - address sender = _msgSender(); - require(balanceOf(sender) >= amount, "Insufficient balance"); - if (sender == founder) { - require(block.timestamp >= matureAt, "Not mature yet"); - } - - _burn(sender, amount); - _balanceCheckpoints[sender].push( - clock(), - SafeCast.toUint208(balanceOf(sender)) - ); - - IERC20(assetToken).safeTransfer(sender, amount); - } - - function getPastBalanceOf( - address account, - uint256 timepoint - ) public view returns (uint256) { - uint48 currentTimepoint = clock(); - if (timepoint >= currentTimepoint) { - revert ERC5805FutureLookup(timepoint, currentTimepoint); - } - return - _balanceCheckpoints[account].upperLookupRecent( - SafeCast.toUint48(timepoint) - ); - } - - // This is non-transferable token - function transfer( - address /*to*/, - uint256 /*value*/ - ) public override returns (bool) { - revert("Transfer not supported"); - } - - function transferFrom( - address /*from*/, - address /*to*/, - uint256 /*value*/ - ) public override returns (bool) { - revert("Transfer not supported"); - } - - function approve( - address /*spender*/, - uint256 /*value*/ - ) public override returns (bool) { - revert("Approve not supported"); - } - - // The following functions are overrides required by Solidity. - function _update( - address from, - address to, - uint256 value - ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._update(from, to, value); - } - - function getPastDelegates( + function mint( address account, - uint256 timepoint - ) public view returns (address) { - return super._getPastDelegates(account, timepoint); - } - - function mint(address receiver, uint256 amount) external{ - return _mint(receiver, amount); + uint256 amount + ) public onlyRole(MINTER_ROLE) { + _mint(account, amount); } } diff --git a/contracts/virtualPersona/IAgentDAO.sol b/contracts/virtualPersona/IAgentDAO.sol index 829c5e9..651fe39 100644 --- a/contracts/virtualPersona/IAgentDAO.sol +++ b/contracts/virtualPersona/IAgentDAO.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "@openzeppelin/contracts/governance/utils/IVotes.sol"; +import "../governance/IERC1155Votes.sol"; import "@openzeppelin/contracts/governance/IGovernor.sol"; interface IAgentDAO { function initialize( string memory name, - IVotes token, + IERC1155Votes token, + uint256 tokenId, address contributionNft, uint256 threshold, uint32 votingPeriod_ diff --git a/contracts/virtualPersona/IAgentNft.sol b/contracts/virtualPersona/IAgentNft.sol index 95babfe..b34f7ae 100644 --- a/contracts/virtualPersona/IAgentNft.sol +++ b/contracts/virtualPersona/IAgentNft.sol @@ -5,16 +5,18 @@ import "./IValidatorRegistry.sol"; interface IAgentNft is IValidatorRegistry { struct VirtualInfo { - address dao; // Persona DAO can update the persona metadata + address dao; // Agent DAO can update the agent metadata address token; address founder; address tba; // Token Bound Address uint8[] coreTypes; + address pool; // Liquidity pool for the agent } event CoresUpdated(uint256 virtualId, uint8[] coreTypes); function mint( + uint256 id, address to, string memory newTokenURI, address payable theDAO, @@ -50,4 +52,6 @@ interface IAgentNft is IValidatorRegistry { function getAllServices( uint256 virtualId ) external view returns (uint256[] memory); + + function nextVirtualId() external view returns (uint256); } diff --git a/contracts/virtualPersona/IAgentToken.sol b/contracts/virtualPersona/IAgentToken.sol index 88bdb94..50f7093 100644 --- a/contracts/virtualPersona/IAgentToken.sol +++ b/contracts/virtualPersona/IAgentToken.sol @@ -5,29 +5,9 @@ interface IAgentToken { function initialize( string memory name, string memory symbol, - address theFounder, - address theAssetToken, - address theVirtualNft, - uint256 theMatureAt + address minter ) external; - function stake( - uint256 amount, - address receiver, - address delegatee - ) external; - - function withdraw(uint256 amount) external; - - function getPastDelegates( - address account, - uint256 timepoint - ) external view returns (address); - - function getPastBalanceOf( - address account, - uint256 timepoint - ) external view returns (uint256); function mint(address receiver, uint256 amount) external; } From a7855d36ecb899355e6e43249e3163ba2b20008f Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 23 May 2024 15:40:14 +0800 Subject: [PATCH 04/30] updated voting token --- contracts/virtualPersona/AgentToken.sol | 34 ------ contracts/virtualPersona/AgentVeToken.sol | 135 +++++++++++++++++++++ contracts/virtualPersona/IAgentToken.sol | 13 -- contracts/virtualPersona/IAgentVeToken.sol | 29 +++++ 4 files changed, 164 insertions(+), 47 deletions(-) delete mode 100644 contracts/virtualPersona/AgentToken.sol create mode 100644 contracts/virtualPersona/AgentVeToken.sol delete mode 100644 contracts/virtualPersona/IAgentToken.sol create mode 100644 contracts/virtualPersona/IAgentVeToken.sol diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol deleted file mode 100644 index 19cbcb2..0000000 --- a/contracts/virtualPersona/AgentToken.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; - -import "./IAgentToken.sol"; - -contract AgentToken is IAgentToken, AccessControlUpgradeable, ERC20Upgradeable { - constructor() { - _disableInitializers(); - } - - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - function initialize( - string memory name, - string memory symbol, - address minter - ) external initializer { - __ERC20_init(name, symbol); - __AccessControl_init(); - _grantRole(MINTER_ROLE, minter); - } - - function mint( - address account, - uint256 amount - ) public onlyRole(MINTER_ROLE) { - _mint(account, amount); - } -} diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol new file mode 100644 index 0000000..21b3bb4 --- /dev/null +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import "./IAgentVeToken.sol"; +import "./IAgentNft.sol"; +import "./ERC20Votes.sol"; + +contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { + using SafeERC20 for IERC20; + using Checkpoints for Checkpoints.Trace208; + + address public immutable assetToken; // This is the token that is staked + address public founder; + uint256 public matureAt; // The timestamp when the founder can withdraw the tokens + + constructor(address _assetToken) { + _disableInitializers(); + assetToken = _assetToken; + } + + mapping(address => Checkpoints.Trace208) private _balanceCheckpoints; + + bool internal locked; + + modifier noReentrant() { + require(!locked, "cannot reenter"); + locked = true; + _; + locked = false; + } + + function initialize( + string memory name, + string memory symbol, + address _founder, + uint256 _matureAt + ) external initializer { + __ERC20_init(name, symbol); + __ERC20Votes_init(); + + founder = _founder; + matureAt = _matureAt; + } + + // Stakers have to stake their tokens and delegate to a validator + function stake( + uint256 amount, + address receiver, + address delegatee + ) public { + address sender = _msgSender(); + require(amount > 0, "Cannot stake 0"); + + IERC20(assetToken).safeTransferFrom(sender, address(this), amount); + _mint(receiver, amount); + _delegate(receiver, delegatee); + _balanceCheckpoints[receiver].push( + clock(), + SafeCast.toUint208(balanceOf(receiver)) + ); + } + + function withdraw(uint256 amount) public noReentrant { + address sender = _msgSender(); + require(balanceOf(sender) >= amount, "Insufficient balance"); + if (sender == founder) { + require(block.timestamp >= matureAt, "Not mature yet"); + } + + _burn(sender, amount); + _balanceCheckpoints[sender].push( + clock(), + SafeCast.toUint208(balanceOf(sender)) + ); + + IERC20(assetToken).safeTransfer(sender, amount); + } + + function getPastBalanceOf( + address account, + uint256 timepoint + ) public view returns (uint256) { + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } + return + _balanceCheckpoints[account].upperLookupRecent( + SafeCast.toUint48(timepoint) + ); + } + + // This is non-transferable token + function transfer( + address /*to*/, + uint256 /*value*/ + ) public override returns (bool) { + revert("Transfer not supported"); + } + + function transferFrom( + address /*from*/, + address /*to*/, + uint256 /*value*/ + ) public override returns (bool) { + revert("Transfer not supported"); + } + + function approve( + address /*spender*/, + uint256 /*value*/ + ) public override returns (bool) { + revert("Approve not supported"); + } + + // The following functions are overrides required by Solidity. + function _update( + address from, + address to, + uint256 value + ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._update(from, to, value); + } + + function getPastDelegates( + address account, + uint256 timepoint + ) public view returns (address) { + return super._getPastDelegates(account, timepoint); + } +} \ No newline at end of file diff --git a/contracts/virtualPersona/IAgentToken.sol b/contracts/virtualPersona/IAgentToken.sol deleted file mode 100644 index 50f7093..0000000 --- a/contracts/virtualPersona/IAgentToken.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -interface IAgentToken { - function initialize( - string memory name, - string memory symbol, - address minter - ) external; - - - function mint(address receiver, uint256 amount) external; -} diff --git a/contracts/virtualPersona/IAgentVeToken.sol b/contracts/virtualPersona/IAgentVeToken.sol new file mode 100644 index 0000000..a0b4a9b --- /dev/null +++ b/contracts/virtualPersona/IAgentVeToken.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IAgentVeToken { + function initialize( + string memory name, + string memory symbol, + address _founder, + uint256 _matureAt + ) external; + + function stake( + uint256 amount, + address receiver, + address delegatee + ) external; + + function withdraw(uint256 amount) external; + + function getPastDelegates( + address account, + uint256 timepoint + ) external view returns (address); + + function getPastBalanceOf( + address account, + uint256 timepoint + ) external view returns (uint256); +} From d5a6a993b6f9b73796d35254d250d23d5dcf5157 Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 23 May 2024 15:50:11 +0800 Subject: [PATCH 05/30] added agent token --- contracts/virtualPersona/AgentToken.sol | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 contracts/virtualPersona/AgentToken.sol diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol new file mode 100644 index 0000000..1420108 --- /dev/null +++ b/contracts/virtualPersona/AgentToken.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +contract AgentToken is ERC20Upgradeable { + address public minter; + + modifier onlyMinter() { + require(_msgSender() == minter, "Caller is not the minter"); + _; + } + + constructor() { + _disableInitializers(); + } + + function initialize( + string memory name, + string memory symbol, + address _minter + ) external initializer { + __ERC20_init(name, symbol); + minter = _minter; + } + + function mint(address account, uint256 value) public onlyMinter { + super._mint(account, value); + } +} From ef752071f8b27c80336e4f2d1b813cd8acef9ae1 Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 23 May 2024 16:31:12 +0800 Subject: [PATCH 06/30] factory is ready for first test --- contracts/virtualPersona/AgentFactory.sol | 67 +++++++++++++++++----- contracts/virtualPersona/AgentToken.sol | 3 +- contracts/virtualPersona/AgentVeToken.sol | 15 ++--- contracts/virtualPersona/IAgentToken.sol | 12 ++++ contracts/virtualPersona/IAgentVeToken.sol | 1 + 5 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 contracts/virtualPersona/IAgentToken.sol diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index 89947c0..c80ca5b 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -10,6 +10,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./IAgentToken.sol"; +import "./IAgentVeToken.sol"; import "./IAgentDAO.sol"; import "./IAgentNft.sol"; import "../libs/IERC6551Registry.sol"; @@ -25,6 +26,7 @@ contract AgentFactory is Initializable, AccessControl { IUniswapV2Router02 internal _uniswapRouter; address public tokenImplementation; + address public veTokenImplementation; address public daoImplementation; address public nft; address public tbaRegistry; // Token bound account @@ -42,6 +44,7 @@ contract AgentFactory is Initializable, AccessControl { event NewPersona( uint256 virtualId, address token, + address veToken, address dao, address tba, address lp @@ -79,6 +82,9 @@ contract AgentFactory is Initializable, AccessControl { bool internal locked; + // V2 Storage + address[] public allTradingTokens; + modifier noReentrant() { require(!locked, "cannot reenter"); locked = true; @@ -95,24 +101,25 @@ contract AgentFactory is Initializable, AccessControl { function initialize( address tokenImplementation_, + address veTokenImplementation_, address daoImplementation_, address tbaRegistry_, address assetToken_, address nft_, uint256 applicationThreshold_, - uint256 maturityDuration_, address vault_, address minter_, address uniswapRouter_, - address voteToken_ + address voteToken_, + uint256 maturityDuration_ ) public initializer { tokenImplementation = tokenImplementation_; + veTokenImplementation = veTokenImplementation_; daoImplementation = daoImplementation_; assetToken = assetToken_; tbaRegistry = tbaRegistry_; nft = nft_; applicationThreshold = applicationThreshold_; - maturityDuration = maturityDuration_; _nextId = 1; _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(WITHDRAW_ROLE, msg.sender); @@ -120,6 +127,7 @@ contract AgentFactory is Initializable, AccessControl { minter = minter_; _uniswapRouter = IUniswapV2Router02(uniswapRouter_); voteToken = IERC1155Votes(voteToken_); + maturityDuration = maturityDuration_; } function getApplication( @@ -227,6 +235,14 @@ contract AgentFactory is Initializable, AccessControl { address lp = _createLP(token, assetToken); uint256 virtualId = IAgentNft(nft).nextVirtualId(); + // Create Staking Token + address veToken = _createNewAgentVeToken( + string.concat("Staked ", application.name), + string.concat("s", application.symbol), + lp, + application.proposer + ); + string memory daoName = string.concat(application.name, " DAO"); address payable dao = payable( _createNewDAO( @@ -265,20 +281,20 @@ contract AgentFactory is Initializable, AccessControl { // Call minter to mint initial tokens // Stake in LP // Add liquidity - IAgentToken(token).mint(address(this), 10 ** 18); - IERC20(token).transfer(lp, 10 ** 18); - IERC20(assetToken).transfer(lp, 10 ** 18); - IUniswapV2Pair(lp).mint(address(this)); + IAgentToken(token).mint(address(this), application.withdrawableAmount); + IERC20(token).transfer(lp, application.withdrawableAmount ** 18); + IERC20(assetToken).transfer(lp, application.withdrawableAmount ** 18); + uint256 liq = IUniswapV2Pair(lp).mint(address(this)); // TODO: Check how much LP token minted // IERC20(assetToken).forceApprove(token, application.withdrawableAmount); - // IAgentToken(token).stake( - // application.withdrawableAmount, - // application.proposer, - // application.proposer - // ); + IAgentVeToken(token).stake( + liq, + application.proposer, + application.proposer + ); - emit NewPersona(virtualId, token, dao, tbaAddress, lp); + emit NewPersona(virtualId, token, veToken, dao, tbaAddress, lp); } function _createLP( @@ -321,6 +337,25 @@ contract AgentFactory is Initializable, AccessControl { instance = Clones.clone(tokenImplementation); IAgentToken(instance).initialize(name, symbol, minter); + allTradingTokens.push(instance); + return instance; + } + + function _createNewAgentVeToken( + string memory name, + string memory symbol, + address stakingAsset, + address founder + ) internal returns (address instance) { + instance = Clones.clone(tokenImplementation); + IAgentVeToken(instance).initialize( + name, + symbol, + founder, + stakingAsset, + block.timestamp + maturityDuration + ); + allTokens.push(instance); return instance; } @@ -351,4 +386,10 @@ contract AgentFactory is Initializable, AccessControl { function setMinter(address newMinter) public onlyRole(DEFAULT_ADMIN_ROLE) { minter = newMinter; } + + function setMaturityDuration( + uint256 newDuration + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + maturityDuration = newDuration; + } } diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol index 1420108..aabe1a7 100644 --- a/contracts/virtualPersona/AgentToken.sol +++ b/contracts/virtualPersona/AgentToken.sol @@ -2,8 +2,9 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "./IAgentToken.sol"; -contract AgentToken is ERC20Upgradeable { +contract AgentToken is IAgentToken, ERC20Upgradeable { address public minter; modifier onlyMinter() { diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index 21b3bb4..df30874 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -13,13 +13,12 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { using SafeERC20 for IERC20; using Checkpoints for Checkpoints.Trace208; - address public immutable assetToken; // This is the token that is staked + address public assetToken; // This is the token that is staked address public founder; uint256 public matureAt; // The timestamp when the founder can withdraw the tokens - constructor(address _assetToken) { + constructor() { _disableInitializers(); - assetToken = _assetToken; } mapping(address => Checkpoints.Trace208) private _balanceCheckpoints; @@ -36,14 +35,16 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { function initialize( string memory name, string memory symbol, - address _founder, - uint256 _matureAt + address founder_, + address assetToken_, + uint256 matureAt_ ) external initializer { __ERC20_init(name, symbol); __ERC20Votes_init(); - founder = _founder; - matureAt = _matureAt; + founder = founder_; + matureAt = matureAt_; + assetToken = assetToken_; } // Stakers have to stake their tokens and delegate to a validator diff --git a/contracts/virtualPersona/IAgentToken.sol b/contracts/virtualPersona/IAgentToken.sol new file mode 100644 index 0000000..2e1228b --- /dev/null +++ b/contracts/virtualPersona/IAgentToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IAgentToken { + function initialize( + string memory name, + string memory symbol, + address _minter + ) external; + + function mint(address account, uint256 value) external; +} diff --git a/contracts/virtualPersona/IAgentVeToken.sol b/contracts/virtualPersona/IAgentVeToken.sol index a0b4a9b..d170f6c 100644 --- a/contracts/virtualPersona/IAgentVeToken.sol +++ b/contracts/virtualPersona/IAgentVeToken.sol @@ -6,6 +6,7 @@ interface IAgentVeToken { string memory name, string memory symbol, address _founder, + address _assetToken, uint256 _matureAt ) external; From 4ab9a9f6118a04750c4ff5dfd5def737dc6e2b49 Mon Sep 17 00:00:00 2001 From: kw Date: Tue, 28 May 2024 16:31:11 +0800 Subject: [PATCH 07/30] completed virtual genesis --- contracts/governance/AgentVotes.sol | 105 -- contracts/governance/ERC1155V.sol | 570 ----------- .../governance/GovernorVotesUpgradeable.sol | 104 -- contracts/governance/IERC1155V.sol | 105 -- contracts/governance/IERC1155Votes.sol | 8 - contracts/token/IMinter.sol | 2 +- contracts/token/Minter.sol | 18 +- contracts/virtualPersona/AgentDAO.sol | 26 +- contracts/virtualPersona/AgentFactory.sol | 141 ++- contracts/virtualPersona/AgentNft.sol | 8 +- contracts/virtualPersona/AgentToken.sol | 10 +- contracts/virtualPersona/AgentVeToken.sol | 30 +- contracts/virtualPersona/IAgentDAO.sol | 5 +- contracts/virtualPersona/IAgentFactory.sol | 23 + contracts/virtualPersona/IAgentToken.sol | 3 +- contracts/virtualPersona/IAgentVeToken.sol | 3 +- .../virtualPersona/IExecutionInterface.sol | 6 + .../virtualPersona/IValidatorRegistry.sol | 6 +- hardhat.config.js | 3 + test/kwtest.js | 43 + test/virtualGenesis.js | 922 +++++++++++------- 21 files changed, 783 insertions(+), 1358 deletions(-) delete mode 100644 contracts/governance/AgentVotes.sol delete mode 100644 contracts/governance/ERC1155V.sol delete mode 100644 contracts/governance/GovernorVotesUpgradeable.sol delete mode 100644 contracts/governance/IERC1155V.sol delete mode 100644 contracts/governance/IERC1155Votes.sol create mode 100644 contracts/virtualPersona/IAgentFactory.sol create mode 100644 contracts/virtualPersona/IExecutionInterface.sol create mode 100644 test/kwtest.js diff --git a/contracts/governance/AgentVotes.sol b/contracts/governance/AgentVotes.sol deleted file mode 100644 index aa5c280..0000000 --- a/contracts/governance/AgentVotes.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/access/AccessControl.sol"; -import {ERC1155V} from "./ERC1155V.sol"; -import "../virtualPersona/IAgentNft.sol"; - -// Voting token for agents -// AgentLP token holders can stake their tokens here in exchange for voting token that they can delegate for voting power -contract AgentVotes is ERC1155V, AccessControl, Initializable { - using SafeERC20 for IERC20; - bool public isAdminUnlock = false; - mapping(address => mapping(uint256 => uint256)) lockedAmounts; - IAgentNft _agentNft; - - bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); - - error Locked(); - - bool internal entranceLocked; - - modifier noReentrant() { - require(!entranceLocked, "cannot reenter"); - entranceLocked = true; - _; - entranceLocked = false; - } - - constructor() { - _disableInitializers(); - } - - function uri(uint256) public pure override returns (string memory) {} - - function initialize(address agentNft, address admin) external initializer { - _agentNft = IAgentNft(agentNft); - _grantRole(ADMIN_ROLE, admin); - _grantRole(DEFAULT_ADMIN_ROLE, admin); - } - - function setAdminUnlock(bool unlock) public { - isAdminUnlock = unlock; - } - - // Stakers have to stake their tokens and delegate to a validator - function stake( - uint256 id, // Agent Id is equivalent to token Id - uint256 amount, - address receiver, - address delegatee, - bool locked, - bytes calldata data - ) public { - address sender = _msgSender(); - require(amount > 0, "Cannot stake 0"); - - if (locked) { - lockedAmounts[receiver][id] += amount; - } - - // Get token from NFT - IERC20(_lpToken(id)).safeTransferFrom(sender, address(this), amount); - - _mint(receiver, id, amount, data); - _delegate(receiver, delegatee, id); - } - - function withdraw(uint256 id, uint256 amount) public noReentrant { - address sender = _msgSender(); - uint256 balance = balanceOf[sender][id]; - require(balance >= amount, "Insufficient balance"); - - uint256 withdrawable = isAdminUnlock - ? balance - : balance - lockedAmounts[sender][id]; - if (withdrawable < amount) { - revert Locked(); - } - - _burn(sender, id, amount); - - IERC20(_lpToken(id)).safeTransfer(sender, amount); - } - - // Get LP token address from Agent NFT - function _lpToken(uint256 id) internal view returns (address) { - address lpToken = _agentNft.virtualInfo(id).pool; - } - - function supportsInterface( - bytes4 interfaceId - ) public view override(AccessControl, ERC1155V) returns (bool) { - return super.supportsInterface(interfaceId); - } - - function clock() external view returns (uint48) { - return uint48(block.number); - } - - function CLOCK_MODE() external view returns (string memory) { - return "mode=blocknumber&from=default"; - } -} diff --git a/contracts/governance/ERC1155V.sol b/contracts/governance/ERC1155V.sol deleted file mode 100644 index bab0626..0000000 --- a/contracts/governance/ERC1155V.sol +++ /dev/null @@ -1,570 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "./IERC1155Votes.sol"; -import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; - -/// @notice A generic interface for a contract which properly accepts ERC1155 tokens. -/// @author Modified from ERC1155V (https://github.com/kalidao/ERC1155V/blob/main/src/ERC1155V.sol) -abstract contract ERC1155TokenReceiver { - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes calldata - ) external payable virtual returns (bytes4) { - return ERC1155TokenReceiver.onERC1155Received.selector; - } - - function onERC1155BatchReceived( - address, - address, - uint256[] calldata, - uint256[] calldata, - bytes calldata - ) external payable virtual returns (bytes4) { - return ERC1155TokenReceiver.onERC1155BatchReceived.selector; - } -} - -abstract contract ERC1155V is IERC1155Votes { - /// ----------------------------------------------------------------------- - /// Events - /// ----------------------------------------------------------------------- - - event SupplyChanged( - uint256 indexed id, - uint256 previousBalance, - uint256 newBalance - ); - - /// ----------------------------------------------------------------------- - /// Custom Errors - /// ----------------------------------------------------------------------- - - error LengthMismatch(); - - error NotAuthorized(); - - error UnsafeRecipient(); - - error InvalidRecipient(); - - error Undetermined(); - - error Overflow(); - - /// ----------------------------------------------------------------------- - /// ERC1155 Storage - /// ----------------------------------------------------------------------- - - mapping(address => mapping(uint256 => uint256)) public balanceOf; - - mapping(address => mapping(address => bool)) public isApprovedForAll; - - /// ----------------------------------------------------------------------- - /// Voting Storage - /// ----------------------------------------------------------------------- - - mapping(uint256 => uint256) public totalSupply; - - mapping(address => mapping(uint256 => address)) internal _delegates; - - mapping(address => mapping(uint256 => uint256)) public numCheckpoints; - - mapping(address => mapping(uint256 => mapping(uint256 => Checkpoint))) - public checkpoints; - - mapping(uint256 => uint256) public numSupplyCheckpoints; - - mapping(uint256 => mapping(uint256 => Checkpoint)) public supplyCheckpoints; - - struct Checkpoint { - uint40 fromTimestamp; - uint216 amount; - } - - /// ----------------------------------------------------------------------- - /// Metadata Logic - /// ----------------------------------------------------------------------- - - function uri(uint256 id) public view virtual returns (string memory); - - /// ----------------------------------------------------------------------- - /// ERC165 Logic - /// ----------------------------------------------------------------------- - - function supportsInterface( - bytes4 interfaceId - ) public view virtual returns (bool) { - return - interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. - interfaceId == 0xd9b67a26 || // ERC165 interface ID for ERC1155. - interfaceId == 0x0e89341c; // ERC165 interface ID for ERC1155MetadataURI. - } - - /// ----------------------------------------------------------------------- - /// ERC1155 Logic - /// ----------------------------------------------------------------------- - - function balanceOfBatch( - address[] calldata owners, - uint256[] calldata ids - ) public view virtual returns (uint256[] memory balances) { - if (owners.length != ids.length) revert LengthMismatch(); - - balances = new uint256[](owners.length); - - for (uint256 i; i < owners.length; ) { - balances[i] = balanceOf[owners[i]][ids[i]]; - - // Unchecked because the only math done is incrementing - // the array index counter which cannot possibly overflow. - unchecked { - ++i; - } - } - } - - function setApprovalForAll( - address operator, - bool approved - ) public payable virtual { - isApprovedForAll[msg.sender][operator] = approved; - - emit ApprovalForAll(msg.sender, operator, approved); - } - - function safeTransferFrom( - address from, - address to, - uint256 id, - uint256 amount, - bytes calldata data - ) public payable virtual { - if (msg.sender != from && !isApprovedForAll[from][msg.sender]) - revert NotAuthorized(); - - balanceOf[from][id] -= amount; - - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to][id] += amount; - } - - emit TransferSingle(msg.sender, from, to, id, amount); - - if (to.code.length != 0) { - if ( - ERC1155TokenReceiver(to).onERC1155Received( - msg.sender, - from, - id, - amount, - data - ) != ERC1155TokenReceiver.onERC1155Received.selector - ) { - revert UnsafeRecipient(); - } - } else { - if (to == address(0)) { - revert InvalidRecipient(); - } - } - - _moveDelegates(delegates(from, id), delegates(to, id), id, amount); - } - - function safeBatchTransferFrom( - address from, - address to, - uint256[] calldata ids, - uint256[] calldata amounts, - bytes calldata data - ) public payable virtual { - if (ids.length != amounts.length) revert LengthMismatch(); - - if (msg.sender != from && !isApprovedForAll[from][msg.sender]) - revert NotAuthorized(); - - // Storing these outside the loop saves ~15 gas per iteration. - uint256 id; - uint256 amount; - - for (uint256 i; i < ids.length; ) { - id = ids[i]; - amount = amounts[i]; - - balanceOf[from][id] -= amount; - - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to][id] += amount; - } - - _moveDelegates(delegates(from, id), delegates(to, id), id, amount); - - // An array can't have a total length - // larger than the max uint256 value. - unchecked { - ++i; - } - } - - emit TransferBatch(msg.sender, from, to, ids, amounts); - - if (to.code.length != 0) { - if ( - ERC1155TokenReceiver(to).onERC1155BatchReceived( - msg.sender, - from, - ids, - amounts, - data - ) != ERC1155TokenReceiver.onERC1155BatchReceived.selector - ) { - revert UnsafeRecipient(); - } - } else if (to == address(0)) { - revert InvalidRecipient(); - } - } - - /// ----------------------------------------------------------------------- - /// Voting Logic - /// ----------------------------------------------------------------------- - - function delegates( - address account, - uint256 id - ) public view virtual returns (address) { - address current = _delegates[account][id]; - - return current == address(0) ? account : current; - } - - function getVotes( - address account, - uint256 id - ) public view virtual returns (uint256) { - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - uint256 nCheckpoints = numCheckpoints[account][id]; - - return - nCheckpoints != 0 - ? checkpoints[account][id][nCheckpoints - 1].amount - : 0; - } - } - - function getPastVotes( - address account, - uint256 id, - uint256 timestamp - ) public view virtual returns (uint256) { - if (block.timestamp <= timestamp) revert Undetermined(); - - uint256 nCheckpoints = numCheckpoints[account][id]; - - if (nCheckpoints == 0) return 0; - - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - uint256 prevCheckpoint = nCheckpoints - 1; - - if ( - checkpoints[account][id][prevCheckpoint].fromTimestamp <= - timestamp - ) return checkpoints[account][id][prevCheckpoint].amount; - - if (checkpoints[account][id][0].fromTimestamp > timestamp) return 0; - - uint256 lower; - - uint256 upper = prevCheckpoint; - - while (upper > lower) { - uint256 center = upper - (upper - lower) / 2; - - Checkpoint memory cp = checkpoints[account][id][center]; - - if (cp.fromTimestamp == timestamp) { - return cp.amount; - } else if (cp.fromTimestamp < timestamp) { - lower = center; - } else { - upper = center - 1; - } - } - - return checkpoints[account][id][lower].amount; - } - } - - function getPastTotalSupply( - uint256 id, - uint256 timestamp - ) public view returns (uint256) { - if (block.timestamp <= timestamp) revert Undetermined(); - - uint256 nCheckpoints = numSupplyCheckpoints[id]; - - if (nCheckpoints == 0) return 0; - - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - uint256 prevCheckpoint = nCheckpoints - 1; - - if ( - supplyCheckpoints[id][prevCheckpoint].fromTimestamp <= timestamp - ) return supplyCheckpoints[id][prevCheckpoint].amount; - - if (supplyCheckpoints[id][0].fromTimestamp > timestamp) return 0; - - uint256 lower; - - uint256 upper = prevCheckpoint; - - while (upper > lower) { - uint256 center = upper - (upper - lower) / 2; - - Checkpoint memory cp = supplyCheckpoints[id][center]; - - if (cp.fromTimestamp == timestamp) { - return cp.amount; - } else if (cp.fromTimestamp < timestamp) { - lower = center; - } else { - upper = center - 1; - } - } - - return supplyCheckpoints[id][lower].amount; - } - } - - function delegate(address delegatee, uint256 id) public virtual { - _delegate(msg.sender, delegatee, id); - } - - function _delegate(address delegator, address delegatee, uint256 id) internal { - address currentDelegate = delegates(delegator, id); - - _delegates[delegator][id] = delegatee; - - emit DelegateChanged(delegator, currentDelegate, delegatee, id); - - _moveDelegates( - currentDelegate, - delegatee, - id, - balanceOf[delegator][id] - ); - } - - function _moveDelegates( - address srcRep, - address dstRep, - uint256 id, - uint256 amount - ) internal virtual { - if (srcRep != dstRep && amount != 0) { - if (srcRep != address(0)) { - uint256 srcRepNum = numCheckpoints[srcRep][id]; - - uint256 srcRepOld; - - // Won't underflow because decrement only occurs if positive `srcRepNum`. - unchecked { - srcRepOld = srcRepNum != 0 - ? checkpoints[srcRep][id][srcRepNum - 1].amount - : 0; - } - - _writeCheckpoint( - srcRep, - id, - srcRepNum, - srcRepOld, - srcRepOld - amount - ); - } - - if (dstRep != address(0)) { - uint256 dstRepNum = numCheckpoints[dstRep][id]; - - uint256 dstRepOld; - - // Won't underflow because decrement only occurs if positive `dstRepNum`. - unchecked { - dstRepOld = dstRepNum != 0 - ? checkpoints[dstRep][id][dstRepNum - 1].amount - : 0; - } - - _writeCheckpoint( - dstRep, - id, - dstRepNum, - dstRepOld, - dstRepOld + amount - ); - } - } - } - - function _writeSupplyCheckpoint( - uint256 id, - uint256 nCheckpoints, - uint256 oldAmount, - uint256 newAmount - ) internal virtual { - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - if ( - nCheckpoints != 0 && - supplyCheckpoints[id][nCheckpoints - 1].fromTimestamp == - block.timestamp - ) { - supplyCheckpoints[id][nCheckpoints - 1].amount = _safeCastTo216( - newAmount - ); - } else { - supplyCheckpoints[id][nCheckpoints] = Checkpoint( - _safeCastTo40(block.timestamp), - _safeCastTo216(newAmount) - ); - - // Won't realistically overflow. - ++numSupplyCheckpoints[id]; - } - } - - emit SupplyChanged(id, oldAmount, newAmount); - } - - function _writeCheckpoint( - address delegatee, - uint256 id, - uint256 nCheckpoints, - uint256 oldVotes, - uint256 newVotes - ) internal virtual { - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - if ( - nCheckpoints != 0 && - checkpoints[delegatee][id][nCheckpoints - 1].fromTimestamp == - block.timestamp - ) { - checkpoints[delegatee][id][nCheckpoints - 1] - .amount = _safeCastTo216(newVotes); - } else { - checkpoints[delegatee][id][nCheckpoints] = Checkpoint( - _safeCastTo40(block.timestamp), - _safeCastTo216(newVotes) - ); - - // Won't realistically overflow. - ++numCheckpoints[delegatee][id]; - } - } - - emit DelegateVotesChanged(delegatee, id, oldVotes, newVotes); - } - - function _safeCastTo40(uint256 x) internal pure virtual returns (uint40 y) { - if (x >= (1 << 40)) revert Overflow(); - - y = uint40(x); - } - - function _safeCastTo216( - uint256 x - ) internal pure virtual returns (uint216 y) { - if (x >= (1 << 216)) revert Overflow(); - - y = uint216(x); - } - - /// ----------------------------------------------------------------------- - /// Internal ID Logic - /// ----------------------------------------------------------------------- - - function _mint( - address to, - uint256 id, - uint256 amount, - bytes calldata data - ) internal virtual { - _safeCastTo216(totalSupply[id] += amount); - - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to][id] += amount; - } - - emit TransferSingle(msg.sender, address(0), to, id, amount); - - if (to.code.length != 0) { - if ( - ERC1155TokenReceiver(to).onERC1155Received( - msg.sender, - address(0), - id, - amount, - data - ) != ERC1155TokenReceiver.onERC1155Received.selector - ) { - revert UnsafeRecipient(); - } - } else if (to == address(0)) { - revert InvalidRecipient(); - } - - _moveDelegates(address(0), delegates(to, id), id, amount); - - uint256 nCheckpoints = numSupplyCheckpoints[id]; - - uint256 oldAmount; - - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - oldAmount = nCheckpoints != 0 - ? supplyCheckpoints[id][nCheckpoints - 1].amount - : 0; - } - _writeSupplyCheckpoint(id, nCheckpoints, oldAmount, oldAmount + amount); - } - - function _burn(address from, uint256 id, uint256 amount) internal virtual { - balanceOf[from][id] -= amount; - - // Cannot underflow because a user's balance - // will never be larger than the total supply. - unchecked { - totalSupply[id] -= amount; - } - - emit TransferSingle(msg.sender, from, address(0), id, amount); - - _moveDelegates(delegates(from, id), address(0), id, amount); - - uint256 nCheckpoints = numSupplyCheckpoints[id]; - - uint256 oldAmount; - - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - oldAmount = nCheckpoints != 0 - ? supplyCheckpoints[id][nCheckpoints - 1].amount - : 0; - } - _writeSupplyCheckpoint(id, nCheckpoints, oldAmount, oldAmount - amount); - } -} diff --git a/contracts/governance/GovernorVotesUpgradeable.sol b/contracts/governance/GovernorVotesUpgradeable.sol deleted file mode 100644 index 29f6595..0000000 --- a/contracts/governance/GovernorVotesUpgradeable.sol +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorVotes.sol) - -pragma solidity ^0.8.20; - -import {GovernorUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; -import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol"; -import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "./IERC1155Votes.sol"; -import {IERC6372} from "@openzeppelin/contracts/interfaces/IERC6372.sol"; - -/** - * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} - * token. - */ -abstract contract GovernorVotesUpgradeable is - Initializable, - GovernorUpgradeable -{ - uint256 public tokenId; - - /// @custom:storage-location erc7201:openzeppelin.storage.GovernorVotes - struct GovernorVotesStorage { - IERC1155Votes _token; - } - - // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.GovernorVotes")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant GovernorVotesStorageLocation = - 0x3ba4977254e415696610a40ebf2258dbfa0ec6a2ff64e84bfe715ff16977cc00; - - function _getGovernorVotesStorage() - private - pure - returns (GovernorVotesStorage storage $) - { - assembly { - $.slot := GovernorVotesStorageLocation - } - } - - function __GovernorVotes_init( - IERC1155Votes tokenAddress, - uint256 _tokenId - ) internal onlyInitializing { - __GovernorVotes_init_unchained(tokenAddress, _tokenId); - } - - function __GovernorVotes_init_unchained( - IERC1155Votes tokenAddress, - uint256 _tokenId - ) internal onlyInitializing { - GovernorVotesStorage storage $ = _getGovernorVotesStorage(); - $._token = IERC1155Votes(address(tokenAddress)); - tokenId = _tokenId; - } - - /** - * @dev The token that voting power is sourced from. - */ - function token() public view virtual returns (IERC1155Votes) { - GovernorVotesStorage storage $ = _getGovernorVotesStorage(); - return $._token; - } - - /** - * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token - * does not implement EIP-6372. - */ - function clock() public view virtual override returns (uint48) { - try token().clock() returns (uint48 timepoint) { - return timepoint; - } catch { - return Time.blockNumber(); - } - } - - /** - * @dev Machine-readable description of the clock as specified in EIP-6372. - */ - // solhint-disable-next-line func-name-mixedcase - function CLOCK_MODE() public view virtual override returns (string memory) { - try token().CLOCK_MODE() returns ( - string memory clockmode - ) { - return clockmode; - } catch { - return "mode=blocknumber&from=default"; - } - } - - /** - * Read the voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}). - */ - function _getVotes( - address account, - uint256 timepoint, - bytes memory /*params*/ - ) internal view virtual override returns (uint256) { - return token().getPastVotes(account, tokenId, timepoint); - } -} diff --git a/contracts/governance/IERC1155V.sol b/contracts/governance/IERC1155V.sol deleted file mode 100644 index e58151c..0000000 --- a/contracts/governance/IERC1155V.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MIT -// Modified from OpenZeppelin Contracts (last updated v5.0.0) (governance/utils/IVotes.sol) -pragma solidity ^0.8.20; - -interface IERC1155V { - - error VotesExpiredSignature(uint256 expiry); - - event DelegateChanged( - address indexed delegator, - address indexed fromDelegate, - address indexed toDelegate, - uint256 id - ); - - event DelegateVotesChanged( - address indexed delegate, - uint256 indexed id, - uint256 previousBalance, - uint256 newBalance - ); - - event TransferSingle( - address indexed operator, - address indexed from, - address indexed to, - uint256 id, - uint256 amount - ); - - event TransferBatch( - address indexed operator, - address indexed from, - address indexed to, - uint256[] ids, - uint256[] amounts - ); - - event ApprovalForAll( - address indexed owner, - address indexed operator, - bool approved - ); - - event TransferabilitySet( - address indexed operator, - uint256 indexed id, - bool set - ); - - event URI(string value, uint256 indexed id); - - /** - * @dev Returns the current amount of votes that `account` has. - */ - - function getVotes(address account, uint256 id) external view returns (uint256); - - /** - * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is - * configured to use block numbers, this will return the value at the end of the corresponding block. - */ - function getPastVotes( - address account, - uint256 id, - uint256 timepoint - ) external view returns (uint256); - - /** - * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is - * configured to use block numbers, this will return the value at the end of the corresponding block. - * - * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. - * Votes that have not been delegated are still part of total supply, even though they would not participate in a - * vote. - */ - function getPastTotalSupply( - uint256 id, - uint256 timepoint - ) external view returns (uint256); - - /** - * @dev Returns the delegate that `account` has chosen. - */ - function delegates(address account, uint256 id) external view returns (address); - - /** - * @dev Delegates votes from the sender to `delegatee`. - */ - function delegate(address delegatee, uint256 id) external; - - // TODO - // /** - // * @dev Delegates votes from signer to `delegatee`. - // */ - // function delegateBySig( - // address delegatee, - // uint256 id, - // uint256 nonce, - // uint256 expiry, - // uint8 v, - // bytes32 r, - // bytes32 s - // ) external; -} diff --git a/contracts/governance/IERC1155Votes.sol b/contracts/governance/IERC1155Votes.sol deleted file mode 100644 index 2e76f29..0000000 --- a/contracts/governance/IERC1155Votes.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -// Modified from OpenZeppelin Contracts (last updated v5.0.0) (governance/utils/IVotes.sol) -pragma solidity ^0.8.20; - -import "./IERC1155V.sol"; -import {IERC6372} from "@openzeppelin/contracts/interfaces/IERC6372.sol"; - -interface IERC1155Votes is IERC1155V, IERC6372 {} diff --git a/contracts/token/IMinter.sol b/contracts/token/IMinter.sol index 8233044..11fdaf7 100644 --- a/contracts/token/IMinter.sol +++ b/contracts/token/IMinter.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; interface IMinter { - function mintInitial(address token) external; + function mintInitial(address , uint256 amount) external; function mint(uint256 nftId) external; } diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol index 072e27e..1e1a538 100644 --- a/contracts/token/Minter.sol +++ b/contracts/token/Minter.sol @@ -11,7 +11,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Minter is IMinter, Ownable { +contract Minter is IMinter, Ownable { address public serviceNft; address public contributionNft; address public agentNft; @@ -20,9 +20,7 @@ contract Minter is IMinter, Ownable { uint256 public ipShare; // Share for IP holder uint256 public dataShare; // Share for Dataset provider - uint256 public bootstrapAmount; // Amount for initial LP bootstrap - - mapping(uint256 => bool) _minted; + mapping(uint256 => bool) _mintedNfts; bool internal locked; @@ -75,12 +73,8 @@ contract Minter is IMinter, Ownable { agentFactory = _factory; } - function setBootstrapAmount(uint256 amount) public onlyOwner { - bootstrapAmount = amount; - } - - function mintInitial(address token) public onlyFactory { - IAgentToken(token).mint(_msgSender(), bootstrapAmount); + function mintInitial(address token, uint256 amount) public onlyFactory { + IAgentToken(token).mint(_msgSender(), amount); } function mint(uint256 nftId) public { @@ -88,7 +82,7 @@ contract Minter is IMinter, Ownable { // 1. ELO impact amount, to be shared between model and dataset owner // 2. IP share amount, ontop of the ELO impact - require(!_minted[nftId], "Already minted"); + require(!_mintedNfts[nftId], "Already minted"); uint256 agentId = IContributionNft(contributionNft).tokenVirtualId( nftId @@ -98,7 +92,7 @@ contract Minter is IMinter, Ownable { address dao = address(IContributionNft(contributionNft).getAgentDAO(agentId)); require(_msgSender() == dao, "Caller is not Agent DAO"); - _minted[nftId] = true; + _mintedNfts[nftId] = true; address tokenAddress = IAgentNft(agentNft).virtualInfo(agentId).token; uint256 datasetId = IContributionNft(contributionNft).getDatasetId( diff --git a/contracts/virtualPersona/AgentDAO.sol b/contracts/virtualPersona/AgentDAO.sol index f477706..ee760a5 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -3,25 +3,22 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorStorageUpgradeable.sol"; -//import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol"; import "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; import "./IAgentDAO.sol"; import "./GovernorCountingSimpleUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "../contribution/IContributionNft.sol"; -import "../governance/IERC1155Votes.sol"; -import "../governance/GovernorVotesUpgradeable.sol"; -//GovernorVotesUpgradeable, -//GovernorVotesQuorumFractionUpgradeable contract AgentDAO is IAgentDAO, GovernorSettingsUpgradeable, GovernorCountingSimpleUpgradeable, GovernorStorageUpgradeable, - GovernorVotesUpgradeable + GovernorVotesUpgradeable, + GovernorVotesQuorumFractionUpgradeable { using Checkpoints for Checkpoints.Trace208; @@ -38,8 +35,7 @@ contract AgentDAO is function initialize( string memory name, - IERC1155Votes token, - uint256 tokenId_, + IVotes token, address contributionNft, uint256 threshold, uint32 votingPeriod_ @@ -47,8 +43,8 @@ contract AgentDAO is __Governor_init(name); __GovernorSettings_init(0, votingPeriod_, threshold); __GovernorCountingSimple_init(); - __GovernorVotes_init(token, tokenId_); - // __GovernorVotesQuorumFraction_init(5100); + __GovernorVotes_init(token); + __GovernorVotesQuorumFraction_init(5100); __GovernorCountingSimple_init(); __GovernorStorage_init(); @@ -217,13 +213,13 @@ contract AgentDAO is ) public view - override(GovernorUpgradeable) + override(GovernorUpgradeable, GovernorVotesQuorumFractionUpgradeable) returns (uint256) { - return token().getPastTotalSupply(tokenId, blockNumber); + return super.quorum(blockNumber); } - // function quorumDenominator() public pure override returns (uint256) { - // return 10000; - // } + function quorumDenominator() public pure override returns (uint256) { + return 10000; + } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index c80ca5b..683bcc6 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -9,6 +9,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./IAgentFactory.sol"; import "./IAgentToken.sol"; import "./IAgentVeToken.sol"; import "./IAgentDAO.sol"; @@ -17,29 +18,25 @@ import "../libs/IERC6551Registry.sol"; import "../pool/IUniswapV2Router02.sol"; import "../pool/IUniswapV2Factory.sol"; import "../pool/IUniswapV2Pair.sol"; -import "../governance/IERC1155Votes.sol"; +import "../token/IMinter.sol"; -contract AgentFactory is Initializable, AccessControl { +contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { using SafeERC20 for IERC20; uint256 private _nextId; - IUniswapV2Router02 internal _uniswapRouter; - address public tokenImplementation; - address public veTokenImplementation; address public daoImplementation; address public nft; address public tbaRegistry; // Token bound account uint256 public applicationThreshold; - IERC1155Votes public voteToken; address[] public allTokens; address[] public allDAOs; address public assetToken; // Base currency - uint256 public maturityDuration; // Maturity duration in seconds + uint256 public maturityDuration; // Staking duration in seconds for initial LP. eg: 100years - bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE"); + bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE"); // Able to withdraw and execute applications event NewPersona( uint256 virtualId, @@ -75,16 +72,21 @@ contract AgentFactory is Initializable, AccessControl { mapping(uint256 => Application) private _applications; + address public gov; // Deprecated in v2, execution of application does not require DAO decision anymore + + modifier onlyGov() { + require(msg.sender == gov, "Only DAO can execute proposal"); + _; + } + event ApplicationThresholdUpdated(uint256 newThreshold); + event GovUpdated(address newGov); event ImplContractsUpdated(address token, address dao); address private _vault; // Vault to hold all Virtual NFTs bool internal locked; - // V2 Storage - address[] public allTradingTokens; - modifier noReentrant() { require(!locked, "cannot reenter"); locked = true; @@ -92,7 +94,15 @@ contract AgentFactory is Initializable, AccessControl { locked = false; } - address minter; + /////////////////////////////////////////////////////////////// + // V2 Storage + /////////////////////////////////////////////////////////////// + address[] public allTradingTokens; + IUniswapV2Router02 internal _uniswapRouter; + address public veTokenImplementation; + address private _minter; + + /////////////////////////////////////////////////////////////// /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -107,11 +117,7 @@ contract AgentFactory is Initializable, AccessControl { address assetToken_, address nft_, uint256 applicationThreshold_, - address vault_, - address minter_, - address uniswapRouter_, - address voteToken_, - uint256 maturityDuration_ + address vault_ ) public initializer { tokenImplementation = tokenImplementation_; veTokenImplementation = veTokenImplementation_; @@ -122,12 +128,7 @@ contract AgentFactory is Initializable, AccessControl { applicationThreshold = applicationThreshold_; _nextId = 1; _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - _grantRole(WITHDRAW_ROLE, msg.sender); _vault = vault_; - minter = minter_; - _uniswapRouter = IUniswapV2Router02(uniswapRouter_); - voteToken = IERC1155Votes(voteToken_); - maturityDuration = maturityDuration_; } function getApplication( @@ -136,7 +137,7 @@ contract AgentFactory is Initializable, AccessControl { return _applications[proposalId]; } - function proposePersona( + function proposeAgent( string memory name, string memory symbol, string memory tokenURI, @@ -206,16 +207,28 @@ contract AgentFactory is Initializable, AccessControl { "Application is not matured yet" ); + uint256 withdrawableAmount = application.withdrawableAmount; + application.withdrawableAmount = 0; application.status = ApplicationStatus.Withdrawn; IERC20(assetToken).safeTransfer( application.proposer, - application.withdrawableAmount + withdrawableAmount ); } function executeApplication(uint256 id) public noReentrant { + // This will bootstrap an Agent with following components: + // C1: Agent Token + // C2: LP Pool + // C3: Agent veToken + // C4: Agent DAO + // C5: Agent NFT + // C6: TBA + // C7: Mint initial Agent tokens + // C8: Provide liquidity + // C9: Stake liquidity token to get veToken require( _applications[id].status == ApplicationStatus.Active, "Application is not active" @@ -223,19 +236,27 @@ contract AgentFactory is Initializable, AccessControl { Application storage application = _applications[id]; + require( + msg.sender == application.proposer || + hasRole(WITHDRAW_ROLE, msg.sender), + "Not proposer" + ); + + uint256 initialSupply = application.withdrawableAmount; // Initial LP supply + application.withdrawableAmount = 0; application.status = ApplicationStatus.Executed; + // C1 address token = _createNewAgentToken( application.name, application.symbol ); - // Create LP + // C2 address lp = _createLP(token, assetToken); - uint256 virtualId = IAgentNft(nft).nextVirtualId(); - // Create Staking Token + // C3 address veToken = _createNewAgentVeToken( string.concat("Staked ", application.name), string.concat("s", application.symbol), @@ -243,17 +264,19 @@ contract AgentFactory is Initializable, AccessControl { application.proposer ); + // C4 string memory daoName = string.concat(application.name, " DAO"); address payable dao = payable( _createNewDAO( daoName, - voteToken, - virtualId, + IVotes(veToken), application.daoVotingPeriod, application.daoThreshold ) ); + // C5 + uint256 virtualId = IAgentNft(nft).nextVirtualId(); IAgentNft(nft).mint( virtualId, _vault, @@ -264,6 +287,7 @@ contract AgentFactory is Initializable, AccessControl { ); application.virtualId = virtualId; + // C6 uint256 chainId; assembly { chainId := chainid() @@ -275,21 +299,29 @@ contract AgentFactory is Initializable, AccessControl { nft, virtualId ); - IAgentNft(nft).setTBA(virtualId, tbaAddress); - // Call minter to mint initial tokens - // Stake in LP - // Add liquidity - IAgentToken(token).mint(address(this), application.withdrawableAmount); - IERC20(token).transfer(lp, application.withdrawableAmount ** 18); - IERC20(assetToken).transfer(lp, application.withdrawableAmount ** 18); - uint256 liq = IUniswapV2Pair(lp).mint(address(this)); - // TODO: Check how much LP token minted - - // IERC20(assetToken).forceApprove(token, application.withdrawableAmount); - IAgentVeToken(token).stake( - liq, + // C7 + IMinter(_minter).mintInitial(token, initialSupply); + + // C8 + IERC20(assetToken).approve(address(_uniswapRouter), initialSupply); + IERC20(token).approve(address(_uniswapRouter), initialSupply); + (, , uint liquidity) = _uniswapRouter.addLiquidity( + token, + assetToken, + initialSupply, + initialSupply, + 0, + 0, + address(this), + block.timestamp + 60 + ); + + // C9 + IERC20(lp).approve(veToken, liquidity); + IAgentVeToken(veToken).stake( + liquidity, application.proposer, application.proposer ); @@ -311,8 +343,7 @@ contract AgentFactory is Initializable, AccessControl { function _createNewDAO( string memory name, - IERC1155Votes token, - uint256 tokenId, + IVotes token, uint32 daoVotingPeriod, uint256 daoThreshold ) internal returns (address instance) { @@ -320,7 +351,6 @@ contract AgentFactory is Initializable, AccessControl { IAgentDAO(instance).initialize( name, token, - tokenId, IAgentNft(nft).getContributionNft(), daoThreshold, daoVotingPeriod @@ -335,7 +365,7 @@ contract AgentFactory is Initializable, AccessControl { string memory symbol ) internal returns (address instance) { instance = Clones.clone(tokenImplementation); - IAgentToken(instance).initialize(name, symbol, minter); + IAgentToken(instance).initialize(name, symbol); allTradingTokens.push(instance); return instance; @@ -347,20 +377,21 @@ contract AgentFactory is Initializable, AccessControl { address stakingAsset, address founder ) internal returns (address instance) { - instance = Clones.clone(tokenImplementation); + instance = Clones.clone(veTokenImplementation); IAgentVeToken(instance).initialize( name, symbol, founder, stakingAsset, - block.timestamp + maturityDuration + block.timestamp + maturityDuration, + address(nft) ); allTokens.push(instance); return instance; } - function totalPersonas() public view returns (uint256) { + function totalAgents() public view returns (uint256) { return allTokens.length; } @@ -384,7 +415,11 @@ contract AgentFactory is Initializable, AccessControl { } function setMinter(address newMinter) public onlyRole(DEFAULT_ADMIN_ROLE) { - minter = newMinter; + _minter = newMinter; + } + + function minter() public view override returns (address) { + return _minter; } function setMaturityDuration( @@ -392,4 +427,10 @@ contract AgentFactory is Initializable, AccessControl { ) public onlyRole(DEFAULT_ADMIN_ROLE) { maturityDuration = newDuration; } + + function setUniswapRouter( + address router + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _uniswapRouter = IUniswapV2Router02(router); + } } diff --git a/contracts/virtualPersona/AgentNft.sol b/contracts/virtualPersona/AgentNft.sol index 7fe818a..b6725eb 100644 --- a/contracts/virtualPersona/AgentNft.sol +++ b/contracts/virtualPersona/AgentNft.sol @@ -123,10 +123,10 @@ contract AgentNft is return _stakingTokenToVirtualId[stakingToken]; } - function addValidator( - uint256 virtualId, - address validator - ) public onlyRole(VALIDATOR_ADMIN_ROLE) { + function addValidator(uint256 virtualId, address validator) public { + if (isValidator(virtualId, validator)) { + return; + } _addValidator(virtualId, validator); _initValidatorScore(virtualId, validator); } diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol index aabe1a7..7796425 100644 --- a/contracts/virtualPersona/AgentToken.sol +++ b/contracts/virtualPersona/AgentToken.sol @@ -3,12 +3,15 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "./IAgentToken.sol"; +import "./IAgentFactory.sol"; contract AgentToken is IAgentToken, ERC20Upgradeable { address public minter; + IAgentFactory private _factory; // Single source of truth + modifier onlyMinter() { - require(_msgSender() == minter, "Caller is not the minter"); + require(_msgSender() == _factory.minter(), "Caller is not the minter"); _; } @@ -18,11 +21,10 @@ contract AgentToken is IAgentToken, ERC20Upgradeable { function initialize( string memory name, - string memory symbol, - address _minter + string memory symbol ) external initializer { __ERC20_init(name, symbol); - minter = _minter; + _factory = IAgentFactory(msg.sender); } function mint(address account, uint256 value) public onlyMinter { diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index df30874..a7f4073 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -13,9 +13,11 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { using SafeERC20 for IERC20; using Checkpoints for Checkpoints.Trace208; - address public assetToken; // This is the token that is staked address public founder; + address public assetToken; // This is the token that is staked + address public agentNft; uint256 public matureAt; // The timestamp when the founder can withdraw the tokens + bool public canStake; // To control private/public agent mode constructor() { _disableInitializers(); @@ -37,7 +39,8 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { string memory symbol, address founder_, address assetToken_, - uint256 matureAt_ + uint256 matureAt_, + address agentNft_ ) external initializer { __ERC20_init(name, symbol); __ERC20Votes_init(); @@ -45,17 +48,23 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { founder = founder_; matureAt = matureAt_; assetToken = assetToken_; + agentNft = agentNft_; } // Stakers have to stake their tokens and delegate to a validator - function stake( - uint256 amount, - address receiver, - address delegatee - ) public { + function stake(uint256 amount, address receiver, address delegatee) public { + require( + canStake || totalSupply() == 0, + "Staking is disabled for private agent" + ); // Either public or first staker + address sender = _msgSender(); require(amount > 0, "Cannot stake 0"); + IAgentNft registry = IAgentNft(agentNft); + uint256 virtualId = registry.stakingTokenToVirtualId(address(this)); + registry.addValidator(virtualId, delegatee); + IERC20(assetToken).safeTransferFrom(sender, address(this), amount); _mint(receiver, amount); _delegate(receiver, delegatee); @@ -65,6 +74,11 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { ); } + function setCanStake(bool _canStake) public { + require(_msgSender() == founder, "Not founder"); + canStake = _canStake; + } + function withdraw(uint256 amount) public noReentrant { address sender = _msgSender(); require(balanceOf(sender) >= amount, "Insufficient balance"); @@ -133,4 +147,4 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { ) public view returns (address) { return super._getPastDelegates(account, timepoint); } -} \ No newline at end of file +} diff --git a/contracts/virtualPersona/IAgentDAO.sol b/contracts/virtualPersona/IAgentDAO.sol index 651fe39..4136eb0 100644 --- a/contracts/virtualPersona/IAgentDAO.sol +++ b/contracts/virtualPersona/IAgentDAO.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "../governance/IERC1155Votes.sol"; import "@openzeppelin/contracts/governance/IGovernor.sol"; +import "@openzeppelin/contracts/governance/utils/IVotes.sol"; interface IAgentDAO { function initialize( string memory name, - IERC1155Votes token, - uint256 tokenId, + IVotes token, address contributionNft, uint256 threshold, uint32 votingPeriod_ diff --git a/contracts/virtualPersona/IAgentFactory.sol b/contracts/virtualPersona/IAgentFactory.sol new file mode 100644 index 0000000..37452f3 --- /dev/null +++ b/contracts/virtualPersona/IAgentFactory.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/governance/IGovernor.sol"; + +interface IAgentFactory { + function proposeAgent( + string memory name, + string memory symbol, + string memory tokenURI, + uint8[] memory cores, + bytes32 tbaSalt, + address tbaImplementation, + uint32 daoVotingPeriod, + uint256 daoThreshold + ) external returns (uint256); + + function withdraw(uint256 id) external; + + function totalAgents() external view returns (uint256); + + function minter() external view returns (address); +} diff --git a/contracts/virtualPersona/IAgentToken.sol b/contracts/virtualPersona/IAgentToken.sol index 2e1228b..6c377d2 100644 --- a/contracts/virtualPersona/IAgentToken.sol +++ b/contracts/virtualPersona/IAgentToken.sol @@ -4,8 +4,7 @@ pragma solidity ^0.8.20; interface IAgentToken { function initialize( string memory name, - string memory symbol, - address _minter + string memory symbol ) external; function mint(address account, uint256 value) external; diff --git a/contracts/virtualPersona/IAgentVeToken.sol b/contracts/virtualPersona/IAgentVeToken.sol index d170f6c..dba6f72 100644 --- a/contracts/virtualPersona/IAgentVeToken.sol +++ b/contracts/virtualPersona/IAgentVeToken.sol @@ -7,7 +7,8 @@ interface IAgentVeToken { string memory symbol, address _founder, address _assetToken, - uint256 _matureAt + uint256 _matureAt, + address agentNft_ ) external; function stake( diff --git a/contracts/virtualPersona/IExecutionInterface.sol b/contracts/virtualPersona/IExecutionInterface.sol new file mode 100644 index 0000000..6ad8ed5 --- /dev/null +++ b/contracts/virtualPersona/IExecutionInterface.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IExecutionInterface { + function execute(address to, uint256 value, bytes memory data, uint8 operation) external returns (bool success); +} \ No newline at end of file diff --git a/contracts/virtualPersona/IValidatorRegistry.sol b/contracts/virtualPersona/IValidatorRegistry.sol index cc95558..62e5f64 100644 --- a/contracts/virtualPersona/IValidatorRegistry.sol +++ b/contracts/virtualPersona/IValidatorRegistry.sol @@ -27,5 +27,9 @@ interface IValidatorRegistry { uint256 index ) external view returns (address); - function totalUptimeScore(uint256 virtualId) external view returns (uint256); + function totalUptimeScore( + uint256 virtualId + ) external view returns (uint256); + + function addValidator(uint256 virtualId, address validator) external; } diff --git a/hardhat.config.js b/hardhat.config.js index f19f850..ad86f1b 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -72,6 +72,9 @@ module.exports = { vaultAccountIds: process.env.FIREBLOCKS_VAULT_ACCOUNT_IDS, }, }, + local:{ + url: "http://127.0.0.1:8545" + }, polygon: { url: "https://rpc-mainnet.maticvigil.com/", accounts: [process.env.PRIVATE_KEY], diff --git a/test/kwtest.js b/test/kwtest.js new file mode 100644 index 0000000..3afd239 --- /dev/null +++ b/test/kwtest.js @@ -0,0 +1,43 @@ +/* +We will test the end-to-end implementation of a Contribution flow till Service. + +1. Prepare 100k tokens +2. Propose a new Persona at AgentFactory +3. Once received proposalId from AgentFactory, create a proposal at ProtocolDAO +4. Vote on the proposal +5. Execute the proposal +*/ +const { parseEther, formatEther, toBeHex } = require("ethers/utils"); +const { ethers } = require("hardhat"); +const abi = ethers.AbiCoder.defaultAbiCoder(); +const { expect } = require("chai"); +const { + loadFixture, + mine, +} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); + +const getExecuteCallData = (factory, proposalId) => { + return factory.interface.encodeFunctionData("executeApplication", [ + proposalId, + ]); +}; + +const getMintServiceCalldata = async (serviceNft, virtualId, hash) => { + return serviceNft.interface.encodeFunctionData("mint", [virtualId, hash]); +}; + +describe("KwTest", function () { + + + before(async function () { + const signers = await ethers.getSigners(); + this.accounts = signers.map((signer) => signer.address); + this.signers = signers; + }); + + it("test router", async function () { + console.log(process.env.UNISWAP_ROUTER,) + const c = await ethers.getContractAt("IUniswapV2Router02", "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D") + console.log(await c.factory()) + }); +}); diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index 859af73..a63032f 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -1,30 +1,29 @@ /* We will test the end-to-end implementation of a Virtual genesis initiation -1. Prepare 100k tokens -2. Propose a new Persona at AgentFactory -3. Once received proposalId from AgentFactory, create a proposal at ProtocolDAO -4. Vote on the proposal -5. Execute the proposal +1. Founder sends 100k $VIRTUAL tokens to factory propose an Agent +2. Founder executes the proposal +3. Factory generates following items: + a. Token (For contribution) + b. DAO + c. Liquidity Pool + d. Agent NFT + e. Staking Token +4. Factory then mint 100k $Agent tokens +5. Factory adds 100k $VIRTUAL and $Agent tokens to the LP in exchange for $ALP +6. Factory stakes the $ALP and set recipient of stake tokens $sALP to founder */ -const { parseEther, toBeHex } = require("ethers/utils"); +const { parseEther, toBeHex, formatEther } = require("ethers/utils"); const { expect } = require("chai"); const { loadFixture, mine, } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); -const getExecuteCallData = (factory, proposalId) => { - return factory.interface.encodeFunctionData("executeApplication", [ - proposalId, - ]); -}; - -describe("AgentFactory", function () { - const PROPOSAL_THRESHOLD = parseEther("100000"); - const QUORUM = parseEther("10000"); - const PROTOCOL_DAO_VOTING_PERIOD = 300; +describe("AgentFactoryV2", function () { + const PROPOSAL_THRESHOLD = parseEther("100000"); //100k const MATURITY_SCORE = toBeHex(2000, 32); // 20% + const IP_SHARE = 1000; // 10% const genesisInput = { name: "Jessica", @@ -34,154 +33,159 @@ describe("AgentFactory", function () { cores: [0, 1, 2], tbaSalt: "0xa7647ac9429fdce477ebd9a95510385b756c757c26149e740abbab0ad1be2f16", - tbaImplementation: ethers.ZeroAddress, + tbaImplementation: process.env.TBA_IMPLEMENTATION, daoVotingPeriod: 600, daoThreshold: 1000000000000000000000n, }; - async function deployBaseContracts() { - const [deployer] = await ethers.getSigners(); - const veToken = await ethers.deployContract( - "veVirtualToken", - [deployer.address], - {} - ); - await veToken.waitForDeployment(); + const getAccounts = async () => { + const [deployer, ipVault, founder, poorMan, trader] = + await ethers.getSigners(); + return { deployer, ipVault, founder, poorMan, trader }; + }; - const demoToken = await ethers.deployContract( - "BMWToken", - [deployer.address], - {} - ); - await demoToken.waitForDeployment(); + async function deployBaseContracts() { + const { deployer, ipVault } = await getAccounts(); - const protocolDAO = await ethers.deployContract( - "VirtualProtocolDAO", - [veToken.target, 0, PROTOCOL_DAO_VOTING_PERIOD, PROPOSAL_THRESHOLD, 500], + const virtualToken = await ethers.deployContract( + "VirtualToken", + [PROPOSAL_THRESHOLD, deployer.address], {} ); - await protocolDAO.waitForDeployment(); + await virtualToken.waitForDeployment(); const AgentNft = await ethers.getContractFactory("AgentNft"); - const personaNft = await upgrades.deployProxy(AgentNft, [deployer.address]); + const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); const contribution = await upgrades.deployProxy( await ethers.getContractFactory("ContributionNft"), - [personaNft.target], + [agentNft.target], {} ); const service = await upgrades.deployProxy( await ethers.getContractFactory("ServiceNft"), - [personaNft.target, contribution.target, process.env.DATASET_SHARES], + [agentNft.target, contribution.target, process.env.DATASET_SHARES], {} ); - await personaNft.setContributionService( - contribution.target, - service.target - ); - - const personaToken = await ethers.deployContract("AgentToken"); - await personaToken.waitForDeployment(); - const personaDAO = await ethers.deployContract("AgentDAO"); - await personaDAO.waitForDeployment(); + await agentNft.setContributionService(contribution.target, service.target); - const tba = await ethers.deployContract("ERC6551Registry"); + // Implementation contracts + const agentToken = await ethers.deployContract("AgentToken"); + await agentToken.waitForDeployment(); + const agentDAO = await ethers.deployContract("AgentDAO"); + await agentDAO.waitForDeployment(); + const agentVeToken = await ethers.deployContract("AgentVeToken"); + await agentVeToken.waitForDeployment(); - const personaFactory = await upgrades.deployProxy( - await ethers.getContractFactory("AgentFactory"), + const agentFactory = await upgrades.deployProxy( + await ethers.getContractFactory("AgentFactoryV2"), [ - personaToken.target, - personaDAO.target, - tba.target, - demoToken.target, - personaNft.target, + agentToken.target, + agentVeToken.target, + agentDAO.target, + process.env.TBA_REGISTRY, + virtualToken.target, + agentNft.target, PROPOSAL_THRESHOLD, - 5, - protocolDAO.target, deployer.address, ] ); - await personaNft.grantRole( - await personaNft.MINTER_ROLE(), - personaFactory.target - ); - - return { veToken, protocolDAO, demoToken, personaFactory, personaNft }; + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + const minter = await ethers.deployContract("Minter", [ + service.target, + contribution.target, + agentNft.target, + IP_SHARE, + ipVault.address, + agentFactory.target, + deployer.address, + ]); + await minter.waitForDeployment(); + await agentFactory.setMinter(minter.target); + await agentFactory.setMaturityDuration(86400 * 365 * 100); // 100years + await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); + + return { virtualToken, agentFactory, agentNft }; } - async function deployGenesisVirtual() { - const contracts = await deployBaseContracts(); - const { personaFactory, veToken, protocolDAO, demoToken } = contracts; - const [deployer] = await ethers.getSigners(); + async function deployWithApplication() { + const base = await deployBaseContracts(); + const { agentFactory, virtualToken } = base; + const { founder } = await getAccounts(); // Prepare tokens for proposal - await demoToken.mint(deployer.address, PROPOSAL_THRESHOLD); - await demoToken.approve(personaFactory.target, PROPOSAL_THRESHOLD); - - await personaFactory.proposePersona( - genesisInput.name, - genesisInput.symbol, - genesisInput.tokenURI, - genesisInput.cores, - genesisInput.tbaSalt, - genesisInput.tbaImplementation, - genesisInput.daoVotingPeriod, - genesisInput.daoThreshold - ); + await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); + await virtualToken + .connect(founder) + .approve(agentFactory.target, PROPOSAL_THRESHOLD); + + const tx = await agentFactory + .connect(founder) + .proposeAgent( + genesisInput.name, + genesisInput.symbol, + genesisInput.tokenURI, + genesisInput.cores, + genesisInput.tbaSalt, + genesisInput.tbaImplementation, + genesisInput.daoVotingPeriod, + genesisInput.daoThreshold + ); - const filter = personaFactory.filters.NewApplication; - const events = await personaFactory.queryFilter(filter, -1); + const filter = agentFactory.filters.NewApplication; + const events = await agentFactory.queryFilter(filter, -1); const event = events[0]; const { id } = event.args; + return { applicationId: id, ...base }; + } - // Create proposal - await veToken.oracleTransfer( - [ethers.ZeroAddress], - [deployer.address], - [parseEther("100000000")] - ); - await veToken.delegate(deployer.address); - - await protocolDAO.propose( - [personaFactory.target], - [0], - [getExecuteCallData(personaFactory, id)], - "Create Jessica" - ); - - const daoFilter = protocolDAO.filters.ProposalCreated; - const daoEvents = await protocolDAO.queryFilter(daoFilter, -1); - const daoEvent = daoEvents[0]; - const daoProposalId = daoEvent.args[0]; + async function deployWithAgent() { + const base = await deployWithApplication(); + const { agentFactory, applicationId } = base; - await protocolDAO.castVote(daoProposalId, 1); - await mine(PROTOCOL_DAO_VOTING_PERIOD); + const { founder } = await getAccounts(); + await agentFactory.connect(founder).executeApplication(applicationId); - await protocolDAO.execute(daoProposalId); - const factoryFilter = personaFactory.filters.NewPersona; - const factoryEvents = await personaFactory.queryFilter(factoryFilter, -1); + const factoryFilter = agentFactory.filters.NewPersona; + const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); const factoryEvent = factoryEvents[0]; - const { virtualId, token, dao } = factoryEvent.args; - const persona = { virtualId, token, dao }; - return { ...contracts, persona }; + const { virtualId, token, veToken, dao, tba, lp } = await factoryEvent.args; + + return { + ...base, + agent: { + virtualId, + token, + veToken, + dao, + tba, + lp, + }, + }; } - before(async function () { - const signers = await ethers.getSigners(); - this.accounts = signers.map((signer) => signer.address); - this.signers = signers; - }); + before(async function () {}); - it("should deny new Persona proposal when insufficient asset token", async function () { - const { personaFactory, personaNft } = await loadFixture( + it("should be able to propose a new agent", async function () { + const { agentFactory, virtualToken } = await loadFixture( deployBaseContracts ); - await expect( - personaFactory.proposePersona( + const { founder } = await getAccounts(); + + // Prepare tokens for proposal + await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); + await virtualToken + .connect(founder) + .approve(agentFactory.target, PROPOSAL_THRESHOLD); + + const tx = await agentFactory + .connect(founder) + .proposeAgent( genesisInput.name, genesisInput.symbol, genesisInput.tokenURI, @@ -190,319 +194,507 @@ describe("AgentFactory", function () { genesisInput.tbaImplementation, genesisInput.daoVotingPeriod, genesisInput.daoThreshold - ) - ).to.be.revertedWith("Insufficient asset token"); - }); - - it("should propose a new Persona", async function () { - const { personaFactory, personaNft, demoToken } = await loadFixture( - deployBaseContracts - ); - - // Prepare tokens for proposal - await demoToken.mint(this.accounts[0], PROPOSAL_THRESHOLD); - expect(await demoToken.balanceOf(this.accounts[0])).to.be.equal( - PROPOSAL_THRESHOLD - ); - await demoToken.approve(personaFactory.target, PROPOSAL_THRESHOLD); - - const tx = await personaFactory.proposePersona( - genesisInput.name, - genesisInput.symbol, - genesisInput.tokenURI, - genesisInput.cores, - genesisInput.tbaSalt, - genesisInput.tbaImplementation, - genesisInput.daoVotingPeriod, - genesisInput.daoThreshold - ); - expect(tx).to.emit(personaFactory, "NewPersona"); + ); + expect(tx).to.emit(agentFactory, "NewApplication"); - expect(await demoToken.balanceOf(this.accounts[0])).to.be.equal(0n); + expect(await virtualToken.balanceOf(founder.address)).to.be.equal(0n); - const filter = personaFactory.filters.NewApplication; - const events = await personaFactory.queryFilter(filter, -1); + const filter = agentFactory.filters.NewApplication; + const events = await agentFactory.queryFilter(filter, -1); const event = events[0]; const { id } = event.args; - expect(id).to.not.be.equal(0n); + expect(id).to.be.equal(1n); }); - it("should allow proposal execution by DAO", async function () { - const { personaFactory, personaNft, demoToken, veToken, protocolDAO } = - await loadFixture(deployBaseContracts); - - const [deployer] = this.signers; - - // Prepare tokens for proposal - await demoToken.mint(this.accounts[0], PROPOSAL_THRESHOLD); - await demoToken.approve(personaFactory.target, PROPOSAL_THRESHOLD); - - await personaFactory.proposePersona( - genesisInput.name, - genesisInput.symbol, - genesisInput.tokenURI, - genesisInput.cores, - genesisInput.tbaSalt, - genesisInput.tbaImplementation, - genesisInput.daoVotingPeriod, - genesisInput.daoThreshold - ); - - const filter = personaFactory.filters.NewApplication; - const events = await personaFactory.queryFilter(filter, -1); - const event = events[0]; - const { id } = event.args; + it("should deny new Persona proposal when insufficient asset token", async function () { + const { agentFactory } = await loadFixture(deployBaseContracts); + const { poorMan } = await getAccounts(); + await expect( + agentFactory + .connect(poorMan) + .proposeAgent( + genesisInput.name, + genesisInput.symbol, + genesisInput.tokenURI, + genesisInput.cores, + genesisInput.tbaSalt, + genesisInput.tbaImplementation, + genesisInput.daoVotingPeriod, + genesisInput.daoThreshold + ) + ).to.be.revertedWith("Insufficient asset token"); + }); - // Create proposal - await veToken.oracleTransfer( - [ethers.ZeroAddress], - [deployer.address], - [parseEther("100000000")] + it("should allow application execution by proposer", async function () { + const { applicationId, agentFactory, virtualToken } = await loadFixture( + deployWithApplication ); - await veToken.delegate(deployer.address); + const { founder } = await getAccounts(); + await expect( + agentFactory.connect(founder).executeApplication(applicationId) + ).to.emit(agentFactory, "NewPersona"); + + // Check genesis components + // C1: Agent Token + // C2: LP Pool + // C3: Agent veToken + // C4: Agent DAO + // C5: Agent NFT + // C6: TBA + // C7: Mint initial Agent tokens + // C8: Provide liquidity + // C9: Stake liquidity token to get veToken + }); - await protocolDAO.propose( - [personaFactory.target], - [0], - [getExecuteCallData(personaFactory, id)], - "LFG" - ); + it("agent component C1: Agent Token", async function () { + const { agent } = await loadFixture(deployWithAgent); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + expect(await agentToken.totalSupply()).to.be.equal(PROPOSAL_THRESHOLD); + }); - const daoFilter = protocolDAO.filters.ProposalCreated; - const daoEvents = await protocolDAO.queryFilter(daoFilter, -1); - const daoEvent = daoEvents[0]; - const daoProposalId = daoEvent.args[0]; + it("agent component C2: LP Pool", async function () { + const { agent, virtualToken } = await loadFixture(deployWithAgent); + const lp = await ethers.getContractAt("IUniswapV2Pair", agent.lp); + const t0 = await lp.token0(); + const t1 = await lp.token1(); - await protocolDAO.castVote(daoProposalId, 1); - await mine(PROTOCOL_DAO_VOTING_PERIOD); + const addresses = [agent.token, virtualToken.target]; + expect(addresses).contain(t0); + expect(addresses).contain(t1); - await expect(protocolDAO.execute(daoProposalId)).to.emit( - personaFactory, - "NewPersona" - ); - const factoryFilter = personaFactory.filters.NewPersona; - const factoryEvents = await personaFactory.queryFilter(factoryFilter, -1); - const factoryEvent = factoryEvents[0]; + const reserves = await lp.getReserves(); + expect(reserves[0]).to.be.equal(PROPOSAL_THRESHOLD); + expect(reserves[1]).to.be.equal(PROPOSAL_THRESHOLD); + }); - const { virtualId, token, dao, tba } = factoryEvent.args; - const persona = { virtualId, token, dao, tba }; - - // Check if the Persona was created successfully - const firstToken = await personaFactory.allTokens(0); - const firstDao = await personaFactory.allDAOs(0); - expect(firstToken).to.not.equal(ethers.ZeroAddress); - expect(firstDao).to.not.equal(ethers.ZeroAddress); - - const AgentDAO = await ethers.getContractFactory("AgentDAO"); - const daoInstance = AgentDAO.attach(dao); - expect(await daoInstance.token()).to.equal(token); - expect(await daoInstance.name()).to.equal(genesisInput.daoName); - expect(await daoInstance.proposalThreshold()).to.equal( - genesisInput.daoThreshold - ); - expect(await daoInstance.votingPeriod()).to.equal( - genesisInput.daoVotingPeriod + it("agent component C3: Agent veToken", async function () { + const { agent } = await loadFixture(deployWithAgent); + const { founder } = await getAccounts(); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const balance = await veToken.balanceOf(founder.address); + expect(parseFloat(formatEther(balance).toString()).toFixed(2)).to.be.equal( + "100000.00" ); + }); - const AgentToken = await ethers.getContractFactory("AgentToken"); - const tokenInstance = AgentToken.attach(token); - expect(await tokenInstance.name()).to.equal(genesisInput.name); - expect(await tokenInstance.symbol()).to.equal(genesisInput.symbol); + it("agent component C4: Agent DAO", async function () { + const { agent } = await loadFixture(deployWithAgent); + const dao = await ethers.getContractAt("AgentDAO", agent.dao); + expect(await dao.token()).to.be.equal(agent.veToken); + expect(await dao.name()).to.be.equal(genesisInput.daoName); + }); - const virtualInfo = await personaNft.virtualInfo(persona.virtualId); - expect(virtualInfo.dao).to.equal(dao); + it("agent component C5: Agent NFT", async function () { + const { agent, agentNft } = await loadFixture(deployWithAgent); + const virtualInfo = await agentNft.virtualInfo(agent.virtualId); + expect(virtualInfo.dao).to.equal(agent.dao); expect(virtualInfo.coreTypes).to.deep.equal(genesisInput.cores); - expect(await personaNft.tokenURI(virtualId)).to.equal( + expect(await agentNft.tokenURI(agent.virtualId)).to.equal( genesisInput.tokenURI ); - expect((await personaNft.virtualInfo(virtualId)).tba).to.equal(tba); - expect(await personaNft.isValidator(virtualId, deployer)).to.equal(true); + expect(virtualInfo.tba).to.equal(agent.tba); + }); - expect(await tokenInstance.balanceOf(deployer)).to.equal( - PROPOSAL_THRESHOLD + it("agent component C6: TBA", async function () { + // TBA means whoever owns the NFT can move the account assets + // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent ); - expect(await tokenInstance.getVotes(deployer)).to.equal(PROPOSAL_THRESHOLD); + const { deployer, poorMan } = await getAccounts(); + + const amount = parseEther("500"); + await virtualToken.mint(agent.tba, amount); + expect(await virtualToken.balanceOf(agent.tba)).to.be.equal(amount); + expect(await virtualToken.balanceOf(poorMan.address)).to.be.equal(0n); + + // Now move it + const data = virtualToken.interface.encodeFunctionData("transfer", [ + poorMan.address, + amount, + ]); + + const tba = await ethers.getContractAt("IExecutionInterface", agent.tba); + + await tba.execute(virtualToken.target, 0, data, 0); + const balance = await virtualToken.balanceOf(poorMan.address); + expect(balance).to.be.equal(amount); }); - it("should allow to stake on new persona", async function () { - const [validator, staker] = this.accounts; - const { persona, demoToken } = await loadFixture(deployGenesisVirtual); - - const AgentToken = await ethers.getContractFactory("AgentToken"); - const tokenInstance = AgentToken.attach(persona.token); - // Prepare tokens for staking - // The validatory should have 100k sToken initially because of the initiation stake - expect(await demoToken.balanceOf(validator)).to.be.equal(0n); - expect(await demoToken.balanceOf(staker)).to.be.equal(0n); - expect(await tokenInstance.balanceOf(validator)).to.be.equal( - PROPOSAL_THRESHOLD + it("agent component C7: Mint initial Agent tokens", async function () { + // TBA means whoever owns the NFT can move the account assets + // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent ); - expect(await tokenInstance.balanceOf(staker)).to.be.equal(0n); - expect(await tokenInstance.getVotes(validator)).to.be.equal( + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + expect(await agentToken.totalSupply()).to.be.equal(PROPOSAL_THRESHOLD); + expect(await agentToken.balanceOf(agent.lp)).to.be.equal( PROPOSAL_THRESHOLD ); - expect(await tokenInstance.getVotes(staker)).to.be.equal(0n); - await demoToken.mint(staker, QUORUM); - - const stakeAmount = parseEther("100"); - await demoToken - .connect(this.signers[1]) - .approve(persona.token, stakeAmount); - await tokenInstance - .connect(this.signers[1]) - .stake(stakeAmount, staker, validator); - - expect(await demoToken.balanceOf(validator)).to.be.equal(0n); - expect(await demoToken.balanceOf(staker)).to.be.equal(QUORUM - stakeAmount); - expect(await tokenInstance.balanceOf(validator)).to.be.equal( - PROPOSAL_THRESHOLD - ); - expect(await tokenInstance.balanceOf(staker)).to.be.equal(stakeAmount); - expect(await tokenInstance.getVotes(validator)).to.be.equal( - stakeAmount + PROPOSAL_THRESHOLD - ); - expect(await tokenInstance.getVotes(staker)).to.be.equal(0n); }); - it("should not allow staking and delegate to non-validator", async function () { - const [validator, staker] = this.accounts; - const { persona, demoToken } = await loadFixture(deployGenesisVirtual); - const AgentToken = await ethers.getContractFactory("AgentToken"); - const tokenInstance = AgentToken.attach(persona.token); - - await demoToken.mint(staker, QUORUM); - const stakeAmount = parseEther("100"); - await demoToken - .connect(this.signers[1]) - .approve(persona.token, stakeAmount); - await expect( - tokenInstance.connect(this.signers[1]).stake(stakeAmount, staker, staker) - ).to.be.revertedWith("Delegatee is not a validator"); + it("agent component C8: Provide liquidity", async function () { + // TBA means whoever owns the NFT can move the account assets + // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent + ); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + const { trader } = await getAccounts(); + // Swap + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + const amountToBuy = parseEther("90"); + const capital = parseEther("100"); + await virtualToken.mint(trader.address, capital); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agent.token], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + expect( + parseFloat(formatEther(await virtualToken.balanceOf(trader.address))) + ).to.be.at.most(10); + expect( + parseFloat(formatEther(await agentToken.balanceOf(trader.address))) + ).to.be.equal(90); }); - it("should be able to set new validator and receive delegation", async function () { - const [validator, staker] = this.accounts; - const { persona, demoToken, personaNft } = await loadFixture( - deployGenesisVirtual - ); - const AgentToken = await ethers.getContractFactory("AgentToken"); - const tokenInstance = AgentToken.attach(persona.token); - - await demoToken.mint(staker, QUORUM); - const stakeAmount = parseEther("100"); - await demoToken - .connect(this.signers[1]) - .approve(persona.token, stakeAmount); - await expect( - tokenInstance.connect(this.signers[1]).stake(stakeAmount, staker, staker) - ).to.be.revertedWith("Delegatee is not a validator"); + it("agent component C9: Stake liquidity token to get veToken", async function () { + // TBA means whoever owns the NFT can move the account assets + // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent + ); + const { trader } = await getAccounts(); + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + + // Buy tokens + const amountToBuy = parseEther("90"); + const capital = parseEther("200"); + await virtualToken.mint(trader.address, capital); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agent.token], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + //// + // Start providing liquidity + const lpToken = await ethers.getContractAt("ERC20", agent.lp); + expect(await lpToken.balanceOf(trader.address)).to.be.equal(0n); + await agentToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, amountToBuy); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + await router + .connect(trader) + .addLiquidity( + agentToken.target, + virtualToken.target, + amountToBuy, + capital, + 0, + 0, + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); - await personaNft.addValidator(persona.virtualId, staker); + expect( + parseFloat(formatEther(await lpToken.balanceOf(trader.address))) + ).to.be.at.least(90); + expect(await agentToken.balanceOf(trader.address)).to.be.equal(0n); + }); + it("should allow staking on public agent", async function () { + // Need to provide LP first + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent + ); + const { trader, poorMan, founder } = await getAccounts(); + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + + // Buy tokens + const amountToBuy = parseEther("90"); + const capital = parseEther("200"); + await virtualToken.mint(trader.address, capital); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agent.token], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + //// + // Start providing liquidity + const lpToken = await ethers.getContractAt("ERC20", agent.lp); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).setCanStake(true); + expect(await lpToken.balanceOf(trader.address)).to.be.equal(0n); + await agentToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, amountToBuy); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + await router + .connect(trader) + .addLiquidity( + agentToken.target, + virtualToken.target, + amountToBuy, + capital, + 0, + 0, + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + ///////////////// + // Staking, and able to delegate to anyone + await lpToken.connect(trader).approve(agent.veToken, parseEther("10")); + await veToken + .connect(trader) + .stake(parseEther("10"), trader.address, poorMan.address); + }); + + it("should deny staking on private agent", async function () { + // Need to provide LP first + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent + ); + const { trader, poorMan } = await getAccounts(); + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + + // Buy tokens + const amountToBuy = parseEther("90"); + const capital = parseEther("200"); + await virtualToken.mint(trader.address, capital); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agent.token], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + //// + // Start providing liquidity + const lpToken = await ethers.getContractAt("ERC20", agent.lp); + expect(await lpToken.balanceOf(trader.address)).to.be.equal(0n); + await agentToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, amountToBuy); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + await router + .connect(trader) + .addLiquidity( + agentToken.target, + virtualToken.target, + amountToBuy, + capital, + 0, + 0, + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + ///////////////// + // Staking + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); await expect( - tokenInstance.connect(this.signers[1]).stake(stakeAmount, staker, staker) - ).to.not.be.revertedWith("Delegatee is not a validator"); + veToken + .connect(trader) + .stake(parseEther("10"), trader.address, poorMan.address) + ).to.be.revertedWith("Staking is disabled for private agent"); }); it("should be able to set new validator and able to update score", async function () { - const [validator, staker] = this.accounts; - const { persona, demoToken, personaNft, personaFactory } = - await loadFixture(deployGenesisVirtual); - const AgentDAO = await ethers.getContractFactory("AgentDAO"); - const personaDAO = AgentDAO.attach(persona.dao); - expect( - await personaNft.validatorScore(persona.virtualId, validator) - ).to.be.equal(0n); - expect(await personaDAO.proposalCount()).to.be.equal(0n); + // Need to provide LP first + const { agent, agentNft } = await loadFixture(deployWithAgent); + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + const { founder, poorMan } = await getAccounts(); + expect(await agentDAO.proposalCount()).to.be.equal(0n); // First proposal - const tx = await personaDAO.propose( - [validator], + const tx = await agentDAO.propose( + [founder.address], [0], ["0x"], "First proposal" ); - const filter = personaDAO.filters.ProposalCreated; - const events = await personaDAO.queryFilter(filter, -1); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); const event = events[0]; const { proposalId } = event.args; - expect(await personaDAO.proposalCount()).to.be.equal(1n); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(poorMan.address); + + expect(await agentDAO.proposalCount()).to.be.equal(1n); + const blockNumber = await ethers.provider.getBlockNumber(); + expect( - await personaNft.validatorScore(persona.virtualId, validator) + await agentDAO.getPastScore(poorMan.address, blockNumber - 1) ).to.be.equal(0n); // Deliberation does not count as vote - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 3, - "", - MATURITY_SCORE - ); + await agentDAO + .connect(poorMan) + .castVoteWithReasonAndParams(proposalId, 3, "", MATURITY_SCORE); expect( - await personaNft.validatorScore(persona.virtualId, validator) + await agentNft.validatorScore(agent.virtualId, poorMan.address) ).to.be.equal(0n); // Normal votes - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 1, - "", - MATURITY_SCORE - ); + await agentDAO + .connect(poorMan) + .castVoteWithReasonAndParams(proposalId, 1, "", MATURITY_SCORE); expect( - await personaNft.validatorScore(persona.virtualId, validator) + await agentNft.validatorScore(agent.virtualId, poorMan.address) ).to.be.equal(1n); }); it("should be able to set new validator after created proposals and have correct score", async function () { - const [validator, validator2] = this.accounts; - const { persona, demoToken, personaNft, personaFactory } = - await loadFixture(deployGenesisVirtual); - const AgentDAO = await ethers.getContractFactory("AgentDAO"); - const personaDAO = AgentDAO.attach(persona.dao); - + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent + ); + const { trader, poorMan, founder } = await getAccounts(); + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + + // Get trader to stake on poorMan so that we have 2 validators + // Buy tokens + const amountToBuy = parseEther("90"); + const capital = parseEther("200"); + await virtualToken.mint(trader.address, capital); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agent.token], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + //// + // Start providing liquidity + const lpToken = await ethers.getContractAt("ERC20", agent.lp); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).setCanStake(true); + expect(await lpToken.balanceOf(trader.address)).to.be.equal(0n); + await agentToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, amountToBuy); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + await router + .connect(trader) + .addLiquidity( + agentToken.target, + virtualToken.target, + amountToBuy, + capital, + 0, + 0, + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); // First proposal - await personaDAO.propose([validator], [0], ["0x"], "First proposal"); - await personaDAO.propose([validator], [0], ["0x"], "Second proposal"); + await agentDAO.propose([founder.address], [0], ["0x"], "First proposal"); + await agentDAO.propose([founder.address], [0], ["0x"], "Second proposal"); - const filter = personaDAO.filters.ProposalCreated; - let events = await personaDAO.queryFilter(filter, -1); + const filter = agentDAO.filters.ProposalCreated; + let events = await agentDAO.queryFilter(filter, -1); let event = events[0]; const { proposalId: secondId } = event.args; - await personaDAO.castVoteWithReasonAndParams( - secondId, - 1, - "", - MATURITY_SCORE + await agentDAO + .connect(founder) + .castVoteWithReasonAndParams(secondId, 1, "", MATURITY_SCORE); + + const initialScore = await agentNft.validatorScore( + agent.virtualId, + poorMan.address ); + expect(initialScore).to.be.equal(0n); - // Validator #2 joins when we have 2 proposals - await personaNft.addValidator(persona.virtualId, validator2); - expect( - await personaNft.validatorScore(persona.virtualId, validator) - ).to.be.equal(1n); - expect( - await personaNft.validatorScore(persona.virtualId, validator2) - ).to.be.equal(2n); - expect(await personaDAO.proposalCount()).to.be.equal(2n); - - await personaDAO.propose([validator], [0], ["0x"], "Third proposal"); - events = await personaDAO.queryFilter(filter, -1); - event = events[0]; - const { proposalId: thirdId } = event.args; - await personaDAO - .connect(this.signers[1]) - .castVoteWithReasonAndParams(thirdId, 1, "", MATURITY_SCORE); - expect( - await personaNft.validatorScore(persona.virtualId, validator) - ).to.be.equal(1n); - expect( - await personaNft.validatorScore(persona.virtualId, validator2) - ).to.be.equal(3n); - expect(await personaDAO.proposalCount()).to.be.equal(3n); + // Stake will automatically adds the validator + await lpToken.connect(trader).approve(agent.veToken, parseEther("10")); + await veToken + .connect(trader) + .stake(parseEther("10"), trader.address, poorMan.address); + + const newScore = await agentNft.validatorScore( + agent.virtualId, + poorMan.address + ); + expect(newScore).to.be.equal(2n); + }); + + it("it should allow withdrawal", async function () { + const { applicationId, agentFactory, virtualToken } = await loadFixture( + deployWithApplication + ); + const { founder } = await getAccounts(); + await agentFactory.connect(founder).withdraw(applicationId); + expect(await virtualToken.balanceOf(founder.address)).to.be.equal( + PROPOSAL_THRESHOLD + ); }); }); From f8303f66dff7f4f7a109f62c8d94f7e18fb52d16 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 29 May 2024 15:51:09 +0800 Subject: [PATCH 08/30] added test cases for manual unlocking LP --- contracts/virtualPersona/AgentFactory.sol | 2 +- contracts/virtualPersona/AgentNft.sol | 13 +++++ contracts/virtualPersona/AgentVeToken.sol | 10 ++++ contracts/virtualPersona/IAgentNft.sol | 2 + test/virtualGenesis.js | 71 +++++++++++++++++------ 5 files changed, 79 insertions(+), 19 deletions(-) diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index 683bcc6..ce211e5 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -34,7 +34,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { address[] public allDAOs; address public assetToken; // Base currency - uint256 public maturityDuration; // Staking duration in seconds for initial LP. eg: 100years + uint256 public maturityDuration; // Staking duration in seconds for initial LP. eg: 10years bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE"); // Able to withdraw and execute applications diff --git a/contracts/virtualPersona/AgentNft.sol b/contracts/virtualPersona/AgentNft.sol index b6725eb..35621ed 100644 --- a/contracts/virtualPersona/AgentNft.sol +++ b/contracts/virtualPersona/AgentNft.sol @@ -49,6 +49,11 @@ contract AgentNft is address private _contributionNft; address private _serviceNft; + // V2 Storage + bytes32 public constant ADMIN_ROLE = + keccak256("ADMIN_ROLE"); + mapping(uint256 => bool) private _blacklists; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -253,4 +258,12 @@ contract AgentNft is function totalSupply() public view returns (uint256) { return _nextVirtualId - 1; } + + function isBlacklisted(uint256 virtualId) public view returns (bool) { + return _blacklists[virtualId]; + } + + function setBlacklist(uint256 virtualId, bool value) public onlyRole(ADMIN_ROLE) { + _blacklists[virtualId] = value; + } } diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index a7f4073..83f5a5b 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -8,6 +8,7 @@ import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol import "./IAgentVeToken.sol"; import "./IAgentNft.sol"; import "./ERC20Votes.sol"; +import "@openzeppelin/contracts/access/IAccessControl.sol"; contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { using SafeERC20 for IERC20; @@ -79,6 +80,15 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { canStake = _canStake; } + function setMatureAt(uint256 _matureAt) public { + bytes32 ADMIN_ROLE = keccak256("ADMIN_ROLE"); + require( + IAccessControl(agentNft).hasRole(ADMIN_ROLE, _msgSender()), + "Not admin" + ); + matureAt = _matureAt; + } + function withdraw(uint256 amount) public noReentrant { address sender = _msgSender(); require(balanceOf(sender) >= amount, "Insufficient balance"); diff --git a/contracts/virtualPersona/IAgentNft.sol b/contracts/virtualPersona/IAgentNft.sol index b34f7ae..c13563d 100644 --- a/contracts/virtualPersona/IAgentNft.sol +++ b/contracts/virtualPersona/IAgentNft.sol @@ -54,4 +54,6 @@ interface IAgentNft is IValidatorRegistry { ) external view returns (uint256[] memory); function nextVirtualId() external view returns (uint256); + + function isBlacklisted(uint256 virtualId) external view returns (bool); } diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index a63032f..45fa060 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -105,7 +105,7 @@ describe("AgentFactoryV2", function () { ]); await minter.waitForDeployment(); await agentFactory.setMinter(minter.target); - await agentFactory.setMaturityDuration(86400 * 365 * 100); // 100years + await agentFactory.setMaturityDuration(86400 * 365 * 10); // 100years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); return { virtualToken, agentFactory, agentNft }; @@ -170,7 +170,7 @@ describe("AgentFactoryV2", function () { before(async function () {}); - it("should be able to propose a new agent", async function () { + xit("should be able to propose a new agent", async function () { const { agentFactory, virtualToken } = await loadFixture( deployBaseContracts ); @@ -206,7 +206,7 @@ describe("AgentFactoryV2", function () { expect(id).to.be.equal(1n); }); - it("should deny new Persona proposal when insufficient asset token", async function () { + xit("should deny new Persona proposal when insufficient asset token", async function () { const { agentFactory } = await loadFixture(deployBaseContracts); const { poorMan } = await getAccounts(); await expect( @@ -225,7 +225,7 @@ describe("AgentFactoryV2", function () { ).to.be.revertedWith("Insufficient asset token"); }); - it("should allow application execution by proposer", async function () { + xit("should allow application execution by proposer", async function () { const { applicationId, agentFactory, virtualToken } = await loadFixture( deployWithApplication ); @@ -246,13 +246,13 @@ describe("AgentFactoryV2", function () { // C9: Stake liquidity token to get veToken }); - it("agent component C1: Agent Token", async function () { + xit("agent component C1: Agent Token", async function () { const { agent } = await loadFixture(deployWithAgent); const agentToken = await ethers.getContractAt("AgentToken", agent.token); expect(await agentToken.totalSupply()).to.be.equal(PROPOSAL_THRESHOLD); }); - it("agent component C2: LP Pool", async function () { + xit("agent component C2: LP Pool", async function () { const { agent, virtualToken } = await loadFixture(deployWithAgent); const lp = await ethers.getContractAt("IUniswapV2Pair", agent.lp); const t0 = await lp.token0(); @@ -267,7 +267,7 @@ describe("AgentFactoryV2", function () { expect(reserves[1]).to.be.equal(PROPOSAL_THRESHOLD); }); - it("agent component C3: Agent veToken", async function () { + xit("agent component C3: Agent veToken", async function () { const { agent } = await loadFixture(deployWithAgent); const { founder } = await getAccounts(); const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); @@ -277,14 +277,14 @@ describe("AgentFactoryV2", function () { ); }); - it("agent component C4: Agent DAO", async function () { + xit("agent component C4: Agent DAO", async function () { const { agent } = await loadFixture(deployWithAgent); const dao = await ethers.getContractAt("AgentDAO", agent.dao); expect(await dao.token()).to.be.equal(agent.veToken); expect(await dao.name()).to.be.equal(genesisInput.daoName); }); - it("agent component C5: Agent NFT", async function () { + xit("agent component C5: Agent NFT", async function () { const { agent, agentNft } = await loadFixture(deployWithAgent); const virtualInfo = await agentNft.virtualInfo(agent.virtualId); expect(virtualInfo.dao).to.equal(agent.dao); @@ -297,7 +297,7 @@ describe("AgentFactoryV2", function () { expect(virtualInfo.tba).to.equal(agent.tba); }); - it("agent component C6: TBA", async function () { + xit("agent component C6: TBA", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -323,7 +323,7 @@ describe("AgentFactoryV2", function () { expect(balance).to.be.equal(amount); }); - it("agent component C7: Mint initial Agent tokens", async function () { + xit("agent component C7: Mint initial Agent tokens", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -336,7 +336,7 @@ describe("AgentFactoryV2", function () { ); }); - it("agent component C8: Provide liquidity", async function () { + xit("agent component C8: Provide liquidity", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -373,7 +373,7 @@ describe("AgentFactoryV2", function () { ).to.be.equal(90); }); - it("agent component C9: Stake liquidity token to get veToken", async function () { + xit("agent component C9: Stake liquidity token to get veToken", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -432,7 +432,7 @@ describe("AgentFactoryV2", function () { expect(await agentToken.balanceOf(trader.address)).to.be.equal(0n); }); - it("should allow staking on public agent", async function () { + xit("should allow staking on public agent", async function () { // Need to provide LP first const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent @@ -493,7 +493,7 @@ describe("AgentFactoryV2", function () { .stake(parseEther("10"), trader.address, poorMan.address); }); - it("should deny staking on private agent", async function () { + xit("should deny staking on private agent", async function () { // Need to provide LP first const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent @@ -554,7 +554,7 @@ describe("AgentFactoryV2", function () { ).to.be.revertedWith("Staking is disabled for private agent"); }); - it("should be able to set new validator and able to update score", async function () { + xit("should be able to set new validator and able to update score", async function () { // Need to provide LP first const { agent, agentNft } = await loadFixture(deployWithAgent); const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); @@ -601,7 +601,7 @@ describe("AgentFactoryV2", function () { ).to.be.equal(1n); }); - it("should be able to set new validator after created proposals and have correct score", async function () { + xit("should be able to set new validator after created proposals and have correct score", async function () { const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent ); @@ -687,7 +687,7 @@ describe("AgentFactoryV2", function () { expect(newScore).to.be.equal(2n); }); - it("it should allow withdrawal", async function () { + xit("should allow withdrawal", async function () { const { applicationId, agentFactory, virtualToken } = await loadFixture( deployWithApplication ); @@ -697,4 +697,39 @@ describe("AgentFactoryV2", function () { PROPOSAL_THRESHOLD ); }); + + xit("should lock initial LP", async function () { + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent + ); + // Founder unable to withdraw LP initially + const { founder } = await getAccounts(); + const agentVeToken = await ethers.getContractAt( + "AgentVeToken", + agent.veToken + ); + await expect( + agentVeToken.connect(founder).withdraw(parseEther("10")) + ).to.be.revertedWith("Not mature yet"); + }); + + it("should allow manual unlock LP", async function () { + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent + ); + const {founder, deployer} = await getAccounts() + // Assign admin role + await agentNft.grantRole(await agentNft.ADMIN_ROLE(), deployer); + const agentVeToken = await ethers.getContractAt( + "AgentVeToken", + agent.veToken + ); + await agentNft.setBlacklist(agent.virtualId, true); + expect(await agentNft.isBlacklisted(agent.virtualId)).to.be.equal(true); + await expect(agentVeToken.setMatureAt(0)).to.not.be.reverted; + + await expect( + agentVeToken.connect(founder).withdraw(parseEther("10")) + ).to.not.be.reverted; + }); }); From abad2d3483414680e1e732fc354237d946a3b746 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 5 Jun 2024 11:00:48 +0800 Subject: [PATCH 09/30] added tax function on AgentToken, able to mint AgentToken on contribution --- contracts/token/IMinter.sol | 2 - contracts/token/Minter.sol | 22 +- contracts/virtualPersona/AgentFactory.sol | 109 +- contracts/virtualPersona/AgentNft.sol | 8 +- contracts/virtualPersona/AgentToken.sol | 1376 ++++++++++++++++- contracts/virtualPersona/IAgentNft.sol | 5 +- contracts/virtualPersona/IAgentToken.sol | 357 ++++- .../virtualPersona/IConfigStructures.sol | 109 ++ contracts/virtualPersona/IERC20Config.sol | 29 + contracts/virtualPersona/IErrors.sol | 311 ++++ test/{ => deprecated}/genesisDAO.js | 0 test/{ => deprecated}/rewards.js | 0 test/kwtest.js | 43 - test/rewardsV2.js | 610 +++----- 14 files changed, 2439 insertions(+), 542 deletions(-) create mode 100644 contracts/virtualPersona/IConfigStructures.sol create mode 100644 contracts/virtualPersona/IERC20Config.sol create mode 100644 contracts/virtualPersona/IErrors.sol rename test/{ => deprecated}/genesisDAO.js (100%) rename test/{ => deprecated}/rewards.js (100%) delete mode 100644 test/kwtest.js diff --git a/contracts/token/IMinter.sol b/contracts/token/IMinter.sol index 11fdaf7..8b711fa 100644 --- a/contracts/token/IMinter.sol +++ b/contracts/token/IMinter.sol @@ -2,7 +2,5 @@ pragma solidity ^0.8.20; interface IMinter { - function mintInitial(address , uint256 amount) external; - function mint(uint256 nftId) external; } diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol index 1e1a538..61f9253 100644 --- a/contracts/token/Minter.sol +++ b/contracts/token/Minter.sol @@ -11,7 +11,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Minter is IMinter, Ownable { +contract Minter is IMinter, Ownable { address public serviceNft; address public contributionNft; address public agentNft; @@ -24,6 +24,13 @@ contract Minter is IMinter, Ownable { bool internal locked; + modifier noReentrant() { + require(!locked, "cannot reenter"); + locked = true; + _; + locked = false; + } + address agentFactory; constructor( @@ -46,7 +53,6 @@ contract Minter is IMinter, Ownable { modifier onlyFactory() { require(_msgSender() == agentFactory, "Caller is not Agent Factory"); _; - } function setServiceNft(address serviceAddress) public onlyOwner { @@ -73,14 +79,11 @@ contract Minter is IMinter, Ownable { agentFactory = _factory; } - function mintInitial(address token, uint256 amount) public onlyFactory { - IAgentToken(token).mint(_msgSender(), amount); - } - - function mint(uint256 nftId) public { + function mint(uint256 nftId) public noReentrant { // Mint configuration: // 1. ELO impact amount, to be shared between model and dataset owner // 2. IP share amount, ontop of the ELO impact + // This is safe to be called by anyone as the minted token will be sent to NFT owner only. require(!_mintedNfts[nftId], "Already minted"); @@ -89,16 +92,13 @@ contract Minter is IMinter, Ownable { ); require(agentId != 0, "Agent not found"); - address dao = address(IContributionNft(contributionNft).getAgentDAO(agentId)); - require(_msgSender() == dao, "Caller is not Agent DAO"); - _mintedNfts[nftId] = true; address tokenAddress = IAgentNft(agentNft).virtualInfo(agentId).token; uint256 datasetId = IContributionNft(contributionNft).getDatasetId( nftId ); - uint256 amount = IServiceNft(serviceNft).getImpact(nftId); + uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * 10 ** 18); uint256 ipAmount = (amount * ipShare) / 10000; uint256 dataAmount = 0; diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index ce211e5..3ae5f19 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -15,9 +15,6 @@ import "./IAgentVeToken.sol"; import "./IAgentDAO.sol"; import "./IAgentNft.sol"; import "../libs/IERC6551Registry.sol"; -import "../pool/IUniswapV2Router02.sol"; -import "../pool/IUniswapV2Factory.sol"; -import "../pool/IUniswapV2Pair.sol"; import "../token/IMinter.sol"; contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { @@ -98,9 +95,15 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { // V2 Storage /////////////////////////////////////////////////////////////// address[] public allTradingTokens; - IUniswapV2Router02 internal _uniswapRouter; + address private _uniswapRouter; address public veTokenImplementation; address private _minter; + address private _tokenAdmin; + + // Default agent token params + bytes private _tokenSupplyParams; + bytes private _tokenTaxParams; + uint16 private _tokenMultiplier = 10000; /////////////////////////////////////////////////////////////// @@ -234,6 +237,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { "Application is not active" ); + require(_tokenAdmin != address(0), "Token admin not set"); + Application storage application = _applications[id]; require( @@ -242,7 +247,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { "Not proposer" ); - uint256 initialSupply = application.withdrawableAmount; // Initial LP supply + uint256 initialSupply = (application.withdrawableAmount * + _tokenMultiplier) / 10000; // Initial LP supply application.withdrawableAmount = 0; application.status = ApplicationStatus.Executed; @@ -250,11 +256,10 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { // C1 address token = _createNewAgentToken( application.name, - application.symbol + application.symbol, + initialSupply ); - - // C2 - address lp = _createLP(token, assetToken); + address lp = IAgentToken(token).liquidityPools()[0]; // C3 address veToken = _createNewAgentVeToken( @@ -283,7 +288,9 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { application.tokenURI, dao, application.proposer, - application.cores + application.cores, + lp, + token ); application.virtualId = virtualId; @@ -301,11 +308,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); IAgentNft(nft).setTBA(virtualId, tbaAddress); - // C7 - IMinter(_minter).mintInitial(token, initialSupply); - // C8 - IERC20(assetToken).approve(address(_uniswapRouter), initialSupply); + /*IERC20(assetToken).approve(address(_uniswapRouter), initialSupply); IERC20(token).approve(address(_uniswapRouter), initialSupply); (, , uint liquidity) = _uniswapRouter.addLiquidity( token, @@ -316,31 +320,19 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { 0, address(this), block.timestamp + 60 - ); + );*/ // C9 - IERC20(lp).approve(veToken, liquidity); - IAgentVeToken(veToken).stake( - liquidity, - application.proposer, - application.proposer - ); + // IERC20(lp).approve(veToken, liquidity); + // IAgentVeToken(veToken).stake( + // liquidity, + // application.proposer, + // application.proposer + // ); emit NewPersona(virtualId, token, veToken, dao, tbaAddress, lp); } - function _createLP( - address token_, - address assetToken_ - ) internal returns (address uniswapV2Pair) { - uniswapV2Pair = IUniswapV2Factory(_uniswapRouter.factory()).createPair( - token_, - assetToken_ - ); - - return uniswapV2Pair; - } - function _createNewDAO( string memory name, IVotes token, @@ -362,10 +354,17 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { function _createNewAgentToken( string memory name, - string memory symbol + string memory symbol, + uint256 initialSupply ) internal returns (address instance) { instance = Clones.clone(tokenImplementation); - IAgentToken(instance).initialize(name, symbol); + IAgentToken(instance).initialize( + [_tokenAdmin, _uniswapRouter, assetToken], + abi.encode(name, symbol), + _tokenSupplyParams, + _tokenTaxParams, + initialSupply + ); allTradingTokens.push(instance); return instance; @@ -431,6 +430,44 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { function setUniswapRouter( address router ) public onlyRole(DEFAULT_ADMIN_ROLE) { - _uniswapRouter = IUniswapV2Router02(router); + _uniswapRouter = router; + } + + function setTokenAdmin( + address newTokenAdmin + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _tokenAdmin = newTokenAdmin; + } + + function setTokenSupplyParams( + uint256 maxTokensPerWallet, + uint256 maxTokensPerTxn, + uint256 botProtectionDurationInSeconds + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _tokenSupplyParams = abi.encode( + maxTokensPerWallet, + maxTokensPerTxn, + botProtectionDurationInSeconds + ); + } + + function setTokenTaxParams( + uint256 projectBuyTaxBasisPoints, + uint256 projectSellTaxBasisPoints, + uint256 taxSwapThresholdBasisPoints, + address projectTaxRecipient + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _tokenTaxParams = abi.encode( + projectBuyTaxBasisPoints, + projectSellTaxBasisPoints, + taxSwapThresholdBasisPoints, + projectTaxRecipient + ); + } + + function setTokenMultiplier( + uint16 newMultiplier + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _tokenMultiplier = newMultiplier; } } diff --git a/contracts/virtualPersona/AgentNft.sol b/contracts/virtualPersona/AgentNft.sol index 35621ed..d082a99 100644 --- a/contracts/virtualPersona/AgentNft.sol +++ b/contracts/virtualPersona/AgentNft.sol @@ -92,7 +92,9 @@ contract AgentNft is string memory newTokenURI, address payable theDAO, address founder, - uint8[] memory coreTypes + uint8[] memory coreTypes, + address pool, + address token ) external onlyRole(MINTER_ROLE) returns (uint256) { _nextVirtualId++; _mint(to, virtualId); @@ -102,7 +104,9 @@ contract AgentNft is info.coreTypes = coreTypes; info.founder = founder; IERC5805 daoToken = GovernorVotes(theDAO).token(); - info.token = address(daoToken); + info.veToken = address(daoToken); + info.token = token; + info.pool = pool; _stakingTokenToVirtualId[address(daoToken)] = virtualId; _addValidator(virtualId, founder); _initValidatorScore(virtualId, founder); diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol index 7796425..2444b53 100644 --- a/contracts/virtualPersona/AgentToken.sol +++ b/contracts/virtualPersona/AgentToken.sol @@ -1,12 +1,70 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../pool/IUniswapV2Router02.sol"; +import "../pool/IUniswapV2Factory.sol"; import "./IAgentToken.sol"; import "./IAgentFactory.sol"; -contract AgentToken is IAgentToken, ERC20Upgradeable { - address public minter; +contract AgentToken is + ContextUpgradeable, + IAgentToken, + Ownable2StepUpgradeable +{ + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.Bytes32Set; + using SafeERC20 for IERC20; + + uint256 internal constant BP_DENOM = 10000; + uint256 internal constant ROUND_DEC = 100000000000; + uint256 internal constant CALL_GAS_LIMIT = 50000; + uint256 internal constant MAX_SWAP_THRESHOLD_MULTIPLE = 20; + + address public uniswapV2Pair; + uint256 public botProtectionDurationInSeconds; + bool internal _tokenHasTax; + IUniswapV2Router02 internal _uniswapRouter; + + uint32 public fundedDate; + uint16 public projectBuyTaxBasisPoints; + uint16 public projectSellTaxBasisPoints; + uint16 public swapThresholdBasisPoints; + address public pairToken; // The token used to trade for this token + + /** @dev {_autoSwapInProgress} We start with {_autoSwapInProgress} ON, as we don't want to + * call autoswap when processing initial liquidity from this address. We turn this OFF when + * liquidity has been loaded, and use this bool to control processing during auto-swaps + * from that point onwards. */ + bool private _autoSwapInProgress = true; + + uint128 public maxTokensPerTransaction; + uint128 public maxTokensPerWallet; + address public projectTaxRecipient; + uint128 public projectTaxPendingSwap; + + string private _name; + string private _symbol; + uint256 private _totalSupply; + + /** @dev {_balances} Addresses balances */ + mapping(address => uint256) private _balances; + + /** @dev {_allowances} Addresses allocance details */ + mapping(address => mapping(address => uint256)) private _allowances; + + /** @dev {_validCallerCodeHashes} Code hashes of callers we consider valid */ + EnumerableSet.Bytes32Set private _validCallerCodeHashes; + + /** @dev {_liquidityPools} Enumerable set for liquidity pool addresses */ + EnumerableSet.AddressSet private _liquidityPools; + + /** @dev {_unlimited} Enumerable set for addresses where limits do not apply */ + EnumerableSet.AddressSet private _unlimited; IAgentFactory private _factory; // Single source of truth @@ -20,14 +78,1316 @@ contract AgentToken is IAgentToken, ERC20Upgradeable { } function initialize( - string memory name, - string memory symbol + address[3] memory integrationAddresses_, + bytes memory baseParams_, + bytes memory supplyParams_, + bytes memory taxParams_, + uint256 lpSupply_ ) external initializer { - __ERC20_init(name, symbol); - _factory = IAgentFactory(msg.sender); + _decodeBaseParams(integrationAddresses_[0], baseParams_); + _uniswapRouter = IUniswapV2Router02(integrationAddresses_[1]); + pairToken = integrationAddresses_[2]; + + ERC20SupplyParameters memory supplyParams = abi.decode( + supplyParams_, + (ERC20SupplyParameters) + ); + + ERC20TaxParameters memory taxParams = abi.decode( + taxParams_, + (ERC20TaxParameters) + ); + + maxTokensPerWallet = uint128( + supplyParams.maxTokensPerWallet * (10 ** decimals()) + ); + maxTokensPerTransaction = uint128( + supplyParams.maxTokensPerTxn * (10 ** decimals()) + ); + + botProtectionDurationInSeconds = supplyParams + .botProtectionDurationInSeconds; + + _tokenHasTax = _processTaxParams(taxParams); + swapThresholdBasisPoints = uint16( + taxParams.taxSwapThresholdBasisPoints + ); + projectTaxRecipient = taxParams.projectTaxRecipient; + + _mintBalances(lpSupply_); + + uniswapV2Pair = _createPair(); + + _factory = IAgentFactory(_msgSender()); + } + + /** + * @dev function {_decodeBaseParams} + * + * Decode NFT Parameters + * + * @param projectOwner_ The owner of this contract + * @param encodedBaseParams_ The base params encoded into a bytes array + */ + function _decodeBaseParams( + address projectOwner_, + bytes memory encodedBaseParams_ + ) internal { + _transferOwnership(projectOwner_); + + (_name, _symbol) = abi.decode(encodedBaseParams_, (string, string)); + } + + /** + * @dev function {_processSupplyParams} + * + * Process provided supply params + * + * @param erc20SupplyParameters_ The supply params + */ + function _processSupplyParams( + ERC20SupplyParameters memory erc20SupplyParameters_ + ) internal { + _unlimited.add(owner()); + _unlimited.add(address(this)); + _unlimited.add(address(0)); + } + + /** + * @dev function {_processTaxParams} + * + * Process provided tax params + * + * @param erc20TaxParameters_ The tax params + */ + function _processTaxParams( + ERC20TaxParameters memory erc20TaxParameters_ + ) internal returns (bool tokenHasTax_) { + /** + * @dev If this + * token does NOT have tax applied then there is no need to store or read these parameters, and we can + * avoid this simply by checking the immutable var. Pass back the value for this var from this method. + */ + if ( + erc20TaxParameters_.projectBuyTaxBasisPoints == 0 && + erc20TaxParameters_.projectSellTaxBasisPoints == 0 + ) { + return false; + } else { + projectBuyTaxBasisPoints = uint16( + erc20TaxParameters_.projectBuyTaxBasisPoints + ); + projectSellTaxBasisPoints = uint16( + erc20TaxParameters_.projectSellTaxBasisPoints + ); + return true; + } + } + + /** + * @dev function {_mintBalances} + * + * Mint initial balances + * + * @param lpMint_ The number of tokens for liquidity + */ + function _mintBalances(uint256 lpMint_) internal { + if (lpMint_ > 0) { + _mint(address(this), lpMint_); + } + } + + /** + * @dev function {_createPair} + * + * Create the uniswap pair + * + * @return uniswapV2Pair_ The pair address + */ + function _createPair() internal returns (address uniswapV2Pair_) { + uniswapV2Pair_ = IUniswapV2Factory(_uniswapRouter.factory()).createPair( + address(this), + pairToken + ); + + _liquidityPools.add(uniswapV2Pair_); + emit LiquidityPoolCreated(uniswapV2Pair_); + + _unlimited.add(address(_uniswapRouter)); + _unlimited.add(uniswapV2Pair_); + return (uniswapV2Pair_); + } + + /** + * @dev function {addInitialLiquidity} + * + * Add initial liquidity to the uniswap pair + * + * @param lpOwner The recipient of LP tokens + */ + function addInitialLiquidity(address lpOwner) external onlyOwner { + _addInitialLiquidity(lpOwner); + } + + /** + * @dev function {_addInitialLiquidity} + * + * Add initial liquidity to the uniswap pair (internal function that does processing) + * + * * @param lpOwner The recipient of LP tokens + */ + function _addInitialLiquidity(address lpOwner) internal { + // Funded date is the date of first funding. We can only add initial liquidity once. If this date is set, + // we cannot proceed + if (fundedDate != 0) { + revert InitialLiquidityAlreadyAdded(); + } + + fundedDate = uint32(block.timestamp); + + // Can only do this if this contract holds tokens: + if (balanceOf(address(this)) == 0) { + revert NoTokenForLiquidityPair(); + } + + // Approve the uniswap router for an inifinite amount (max uint256) + // This means that we don't need to worry about later incrememtal + // approvals on tax swaps, as the uniswap router allowance will never + // be decreased (see code in decreaseAllowance for reference) + _approve(address(this), address(_uniswapRouter), type(uint256).max); + IERC20(pairToken).approve(address(_uniswapRouter), type(uint256).max); + // Add the liquidity: + (uint256 amountA, uint256 amountB, uint256 lpTokens) = _uniswapRouter + .addLiquidity( + address(this), + pairToken, + balanceOf(address(this)), + IERC20(pairToken).balanceOf(address(this)), + 0, + 0, + address(this), + block.timestamp + ); + + emit InitialLiquidityAdded(amountA, amountB, lpTokens); + + // We now set this to false so that future transactions can be eligibile for autoswaps + _autoSwapInProgress = false; + + IERC20(uniswapV2Pair).transfer(lpOwner, lpTokens); + } + + /** + * @dev function {isLiquidityPool} + * + * Return if an address is a liquidity pool + * + * @param queryAddress_ The address being queried + * @return bool The address is / isn't a liquidity pool + */ + function isLiquidityPool(address queryAddress_) public view returns (bool) { + /** @dev We check the uniswapV2Pair address first as this is an immutable variable and therefore does not need + * to be fetched from storage, saving gas if this address IS the uniswapV2Pool. We also add this address + * to the enumerated set for ease of reference (for example it is returned in the getter), and it does + * not add gas to any other calls, that still complete in 0(1) time. + */ + return (queryAddress_ == uniswapV2Pair || + _liquidityPools.contains(queryAddress_)); + } + + /** + * @dev function {liquidityPools} + * + * Returns a list of all liquidity pools + * + * @return liquidityPools_ a list of all liquidity pools + */ + function liquidityPools() + external + view + returns (address[] memory liquidityPools_) + { + return (_liquidityPools.values()); + } + + /** + * @dev function {addLiquidityPool} onlyOwner + * + * Allows the manager to add a liquidity pool to the pool enumerable set + * + * @param newLiquidityPool_ The address of the new liquidity pool + */ + function addLiquidityPool(address newLiquidityPool_) public onlyOwner { + // Don't allow calls that didn't pass an address: + if (newLiquidityPool_ == address(0)) { + revert LiquidityPoolCannotBeAddressZero(); + } + // Only allow smart contract addresses to be added, as only these can be pools: + if (newLiquidityPool_.code.length == 0) { + revert LiquidityPoolMustBeAContractAddress(); + } + // Add this to the enumerated list: + _liquidityPools.add(newLiquidityPool_); + emit LiquidityPoolAdded(newLiquidityPool_); + } + + /** + * @dev function {removeLiquidityPool} onlyOwner + * + * Allows the manager to remove a liquidity pool + * + * @param removedLiquidityPool_ The address of the old removed liquidity pool + */ + function removeLiquidityPool( + address removedLiquidityPool_ + ) external onlyOwner { + // Remove this from the enumerated list: + _liquidityPools.remove(removedLiquidityPool_); + emit LiquidityPoolRemoved(removedLiquidityPool_); + } + + /** + * @dev function {isUnlimited} + * + * Return if an address is unlimited (is not subject to per txn and per wallet limits) + * + * @param queryAddress_ The address being queried + * @return bool The address is / isn't unlimited + */ + function isUnlimited(address queryAddress_) public view returns (bool) { + return (_unlimited.contains(queryAddress_)); + } + + /** + * @dev function {unlimitedAddresses} + * + * Returns a list of all unlimited addresses + * + * @return unlimitedAddresses_ a list of all unlimited addresses + */ + function unlimitedAddresses() + external + view + returns (address[] memory unlimitedAddresses_) + { + return (_unlimited.values()); + } + + /** + * @dev function {addUnlimited} onlyOwner + * + * Allows the manager to add an unlimited address + * + * @param newUnlimited_ The address of the new unlimited address + */ + function addUnlimited(address newUnlimited_) external onlyOwner { + // Add this to the enumerated list: + _unlimited.add(newUnlimited_); + emit UnlimitedAddressAdded(newUnlimited_); + } + + /** + * @dev function {removeUnlimited} onlyOwner + * + * Allows the manager to remove an unlimited address + * + * @param removedUnlimited_ The address of the old removed unlimited address + */ + function removeUnlimited(address removedUnlimited_) external onlyOwner { + // Remove this from the enumerated list: + _unlimited.remove(removedUnlimited_); + emit UnlimitedAddressRemoved(removedUnlimited_); + } + + /** + * @dev function {isValidCaller} + * + * Return if an address is a valid caller + * + * @param queryHash_ The code hash being queried + * @return bool The address is / isn't a valid caller + */ + function isValidCaller(bytes32 queryHash_) public view returns (bool) { + return (_validCallerCodeHashes.contains(queryHash_)); + } + + /** + * @dev function {validCallers} + * + * Returns a list of all valid caller code hashes + * + * @return validCallerHashes_ a list of all valid caller code hashes + */ + function validCallers() + external + view + returns (bytes32[] memory validCallerHashes_) + { + return (_validCallerCodeHashes.values()); + } + + /** + * @dev function {addValidCaller} onlyOwner + * + * Allows the owner to add the hash of a valid caller + * + * @param newValidCallerHash_ The hash of the new valid caller + */ + function addValidCaller(bytes32 newValidCallerHash_) external onlyOwner { + _validCallerCodeHashes.add(newValidCallerHash_); + emit ValidCallerAdded(newValidCallerHash_); + } + + /** + * @dev function {removeValidCaller} onlyOwner + * + * Allows the owner to remove a valid caller + * + * @param removedValidCallerHash_ The hash of the old removed valid caller + */ + function removeValidCaller( + bytes32 removedValidCallerHash_ + ) external onlyOwner { + // Remove this from the enumerated list: + _validCallerCodeHashes.remove(removedValidCallerHash_); + emit ValidCallerRemoved(removedValidCallerHash_); + } + + /** + * @dev function {setProjectTaxRecipient} onlyOwner + * + * Allows the manager to set the project tax recipient address + * + * @param projectTaxRecipient_ New recipient address + */ + function setProjectTaxRecipient( + address projectTaxRecipient_ + ) external onlyOwner { + projectTaxRecipient = projectTaxRecipient_; + emit ProjectTaxRecipientUpdated(projectTaxRecipient_); + } + + /** + * @dev function {setSwapThresholdBasisPoints} onlyOwner + * + * Allows the manager to set the autoswap threshold + * + * @param swapThresholdBasisPoints_ New swap threshold in basis points + */ + function setSwapThresholdBasisPoints( + uint16 swapThresholdBasisPoints_ + ) external onlyOwner { + uint256 oldswapThresholdBasisPoints = swapThresholdBasisPoints; + swapThresholdBasisPoints = swapThresholdBasisPoints_; + emit AutoSwapThresholdUpdated( + oldswapThresholdBasisPoints, + swapThresholdBasisPoints_ + ); + } + + /** + * @dev function {setProjectTaxRates} onlyOwner + * + * Change the tax rates, subject to only ever decreasing + * + * @param newProjectBuyTaxBasisPoints_ The new buy tax rate + * @param newProjectSellTaxBasisPoints_ The new sell tax rate + */ + function setProjectTaxRates( + uint16 newProjectBuyTaxBasisPoints_, + uint16 newProjectSellTaxBasisPoints_ + ) external onlyOwner { + uint16 oldBuyTaxBasisPoints = projectBuyTaxBasisPoints; + uint16 oldSellTaxBasisPoints = projectSellTaxBasisPoints; + + projectBuyTaxBasisPoints = newProjectBuyTaxBasisPoints_; + projectSellTaxBasisPoints = newProjectSellTaxBasisPoints_; + + emit ProjectTaxBasisPointsChanged( + oldBuyTaxBasisPoints, + newProjectBuyTaxBasisPoints_, + oldSellTaxBasisPoints, + newProjectSellTaxBasisPoints_ + ); + } + + /** + * @dev function {setLimits} onlyOwner + * + * Change the limits on transactions and holdings + * + * @param newMaxTokensPerTransaction_ The new per txn limit + * @param newMaxTokensPerWallet_ The new tokens per wallet limit + */ + function setLimits( + uint256 newMaxTokensPerTransaction_, + uint256 newMaxTokensPerWallet_ + ) external onlyOwner { + uint256 oldMaxTokensPerTransaction = maxTokensPerTransaction; + uint256 oldMaxTokensPerWallet = maxTokensPerWallet; + // Limit can only be increased: + if ( + (oldMaxTokensPerTransaction == 0 && + newMaxTokensPerTransaction_ != 0) || + (oldMaxTokensPerWallet == 0 && newMaxTokensPerWallet_ != 0) + ) { + revert LimitsCanOnlyBeRaised(); + } + if ( + ((newMaxTokensPerTransaction_ != 0) && + newMaxTokensPerTransaction_ < oldMaxTokensPerTransaction) || + ((newMaxTokensPerWallet_ != 0) && + newMaxTokensPerWallet_ < oldMaxTokensPerWallet) + ) { + revert LimitsCanOnlyBeRaised(); + } + + maxTokensPerTransaction = uint128(newMaxTokensPerTransaction_); + maxTokensPerWallet = uint128(newMaxTokensPerWallet_); + + emit LimitsUpdated( + oldMaxTokensPerTransaction, + newMaxTokensPerTransaction_, + oldMaxTokensPerWallet, + newMaxTokensPerWallet_ + ); + } + + /** + * @dev function {limitsEnforced} + * + * Return if limits are enforced on this contract + * + * @return bool : they are / aren't + */ + function limitsEnforced() public view returns (bool) { + // Limits are not enforced if + // this is renounced AND after then protection end date + // OR prior to LP funding: + // The second clause of 'fundedDate == 0' isn't strictly needed, since with a funded + // date of 0 we would always expect the block.timestamp to be less than 0 plus + // the botProtectionDurationInSeconds. But, to cover the miniscule chance of a user + // selecting a truly enormous bot protection period, such that when added to 0 it + // is more than the current block.timestamp, we have included this second clause. There + // is no permanent gas overhead (the logic will be returning from the first clause after + // the bot protection period has expired). During the bot protection period there is a minor + // gas overhead from evaluating the fundedDate == 0 (which will be false), but this is minimal. + if ( + (owner() == address(0) && + block.timestamp > + fundedDate + botProtectionDurationInSeconds) || fundedDate == 0 + ) { + return false; + } else { + // LP has been funded AND we are within the protection period: + return true; + } + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev totalBuyTaxBasisPoints + * + * Provide easy to view tax total: + */ + function totalBuyTaxBasisPoints() public view returns (uint256) { + return projectBuyTaxBasisPoints; + } + + /** + * @dev totalSellTaxBasisPoints + * + * Provide easy to view tax total: + */ + function totalSellTaxBasisPoints() public view returns (uint256) { + return projectSellTaxBasisPoints; } + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf( + address account + ) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer( + address to, + uint256 amount + ) public virtual override(IERC20) returns (bool) { + address owner = _msgSender(); + _transfer( + owner, + to, + amount, + (isLiquidityPool(owner) || isLiquidityPool(to)) + ); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance( + address owner, + address spender + ) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve( + address spender, + uint256 amount + ) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer( + from, + to, + amount, + (isLiquidityPool(from) || isLiquidityPool(to)) + ); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance( + address spender, + uint256 addedValue + ) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance( + address spender, + uint256 subtractedValue + ) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance < subtractedValue) { + revert AllowanceDecreasedBelowZero(); + } + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer( + address from, + address to, + uint256 amount, + bool applyTax + ) internal virtual { + _beforeTokenTransfer(from, to, amount); + + // Perform pre-tax validation (e.g. amount doesn't exceed balance, max txn amount) + uint256 fromBalance = _pretaxValidationAndLimits(from, to, amount); + + // Perform autoswap if eligible + _autoSwap(from, to); + + // Process taxes + uint256 amountMinusTax = _taxProcessing(applyTax, to, from, amount); + + // Perform post-tax validation (e.g. total balance after post-tax amount applied) + _posttaxValidationAndLimits(from, to, amountMinusTax); + + _balances[from] = fromBalance - amount; + _balances[to] += amountMinusTax; + + emit Transfer(from, to, amountMinusTax); + + _afterTokenTransfer(from, to, amount); + } + + /** + * @dev function {_pretaxValidationAndLimits} + * + * Perform validation on pre-tax amounts + * + * @param from_ From address for the transaction + * @param to_ To address for the transaction + * @param amount_ Amount of the transaction + */ + function _pretaxValidationAndLimits( + address from_, + address to_, + uint256 amount_ + ) internal view returns (uint256 fromBalance_) { + // This can't be a transfer to the liquidity pool before the funding date + // UNLESS the from address is this contract. This ensures that the initial + // LP funding transaction is from this contract using the supply of tokens + // designated for the LP pool, and therefore the initial price in the pool + // is being set as expected. + // + // This protects from, for example, tokens from a team minted supply being + // paired with ETH and added to the pool, setting the initial price, BEFORE + // the initial liquidity is added through this contract. + if (to_ == uniswapV2Pair && from_ != address(this) && fundedDate == 0) { + revert InitialLiquidityNotYetAdded(); + } + + if (from_ == address(0)) { + revert TransferFromZeroAddress(); + } + + if (to_ == address(0)) { + revert TransferToZeroAddress(); + } + + fromBalance_ = _balances[from_]; + + if (fromBalance_ < amount_) { + revert TransferAmountExceedsBalance(); + } + + if ( + limitsEnforced() && + (maxTokensPerTransaction != 0) && + ((isLiquidityPool(from_) && !isUnlimited(to_)) || + (isLiquidityPool(to_) && !isUnlimited(from_))) + ) { + // Liquidity pools aren't always going to round cleanly. This can (and does) + // mean that a limit of 5,000 tokens (for example) will trigger on a transfer + // of 5,000 tokens, as the transfer is actually for 5,000.00000000000000213. + // While 4,999 will work fine, it isn't hugely user friendly. So we buffer + // the limit with rounding decimals, which in all cases are considerably less + // than one whole token: + uint256 roundedLimited; + + unchecked { + roundedLimited = maxTokensPerTransaction + ROUND_DEC; + } + + if (amount_ > roundedLimited) { + revert MaxTokensPerTxnExceeded(); + } + } + + return (fromBalance_); + } + + /** + * @dev function {_posttaxValidationAndLimits} + * + * Perform validation on post-tax amounts + * + * @param to_ To address for the transaction + * @param amount_ Amount of the transaction + */ + function _posttaxValidationAndLimits( + address from_, + address to_, + uint256 amount_ + ) internal view { + if ( + limitsEnforced() && + (maxTokensPerWallet != 0) && + !isUnlimited(to_) && + // If this is a buy (from a liquidity pool), we apply if the to_ + // address isn't noted as unlimited: + (isLiquidityPool(from_) && !isUnlimited(to_)) + ) { + // Liquidity pools aren't always going to round cleanly. This can (and does) + // mean that a limit of 5,000 tokens (for example) will trigger on a max holding + // of 5,000 tokens, as the transfer to achieve that is actually for + // 5,000.00000000000000213. While 4,999 will work fine, it isn't hugely user friendly. + // So we buffer the limit with rounding decimals, which in all cases are considerably + // less than one whole token: + uint256 roundedLimited; + + unchecked { + roundedLimited = maxTokensPerWallet + ROUND_DEC; + } + + if ((amount_ + balanceOf(to_) > roundedLimited)) { + revert MaxTokensPerWalletExceeded(); + } + } + } + + /** + * @dev function {_taxProcessing} + * + * Perform tax processing + * + * @param applyTax_ Do we apply tax to this transaction? + * @param to_ The reciever of the token + * @param from_ The sender of the token + * @param sentAmount_ The amount being send + * @return amountLessTax_ The amount that will be recieved, i.e. the send amount minus tax + */ + function _taxProcessing( + bool applyTax_, + address to_, + address from_, + uint256 sentAmount_ + ) internal returns (uint256 amountLessTax_) { + amountLessTax_ = sentAmount_; + unchecked { + if (_tokenHasTax && applyTax_ && !_autoSwapInProgress) { + uint256 tax; + + // on sell + if (isLiquidityPool(to_) && totalSellTaxBasisPoints() > 0) { + if (projectSellTaxBasisPoints > 0) { + uint256 projectTax = ((sentAmount_ * + projectSellTaxBasisPoints) / BP_DENOM); + projectTaxPendingSwap += uint128(projectTax); + tax += projectTax; + } + } + // on buy + else if ( + isLiquidityPool(from_) && totalBuyTaxBasisPoints() > 0 + ) { + if (projectBuyTaxBasisPoints > 0) { + uint256 projectTax = ((sentAmount_ * + projectBuyTaxBasisPoints) / BP_DENOM); + projectTaxPendingSwap += uint128(projectTax); + tax += projectTax; + } + } + + if (tax > 0) { + _balances[address(this)] += tax; + emit Transfer(from_, address(this), tax); + amountLessTax_ -= tax; + } + } + } + return (amountLessTax_); + } + + /** + * @dev function {_autoSwap} + * + * Automate the swap of accumulated tax fees to native token + * + * @param from_ The sender of the token + * @param to_ The recipient of the token + */ + function _autoSwap(address from_, address to_) internal { + if (_tokenHasTax) { + uint256 contractBalance = balanceOf(address(this)); + uint256 swapBalance = contractBalance; + + uint256 swapThresholdInTokens = (_totalSupply * + swapThresholdBasisPoints) / BP_DENOM; + + if ( + _eligibleForSwap(from_, to_, swapBalance, swapThresholdInTokens) + ) { + // Store that a swap back is in progress: + _autoSwapInProgress = true; + // Check if we need to reduce the amount of tokens for this swap: + if ( + swapBalance > + swapThresholdInTokens * MAX_SWAP_THRESHOLD_MULTIPLE + ) { + swapBalance = + swapThresholdInTokens * + MAX_SWAP_THRESHOLD_MULTIPLE; + } + // Perform the auto swap to pair token + _swapTax(swapBalance, contractBalance); + // Flag that the autoswap is complete: + _autoSwapInProgress = false; + } + } + } + + /** + * @dev function {_eligibleForSwap} + * + * Is the current transfer eligible for autoswap + * + * @param from_ The sender of the token + * @param to_ The recipient of the token + * @param taxBalance_ The current accumulated tax balance + * @param swapThresholdInTokens_ The swap threshold as a token amount + */ + function _eligibleForSwap( + address from_, + address to_, + uint256 taxBalance_, + uint256 swapThresholdInTokens_ + ) internal view returns (bool) { + return (taxBalance_ >= swapThresholdInTokens_ && + !_autoSwapInProgress && + !isLiquidityPool(from_) && + from_ != address(_uniswapRouter) && + to_ != address(_uniswapRouter)); + } + + /** + * @dev function {_swapTax} + * + * Swap tokens taken as tax for pair token + * + * @param swapBalance_ The current accumulated tax balance to swap + * @param contractBalance_ The current accumulated total tax balance + */ + function _swapTax(uint256 swapBalance_, uint256 contractBalance_) internal { + uint256 preSwapBalance = IERC20(pairToken).balanceOf(address(this)); + + address[] memory path = new address[](2); + path[0] = address(this); + path[1] = pairToken; + + // Wrap external calls in try / catch to handle errors + try + _uniswapRouter.swapTokensForExactTokens( + swapBalance_, + 0, + path, + address(this), + block.timestamp + 600 + ) + { + uint256 postSwapBalance = IERC20(pairToken).balanceOf( + address(this) + ); + + uint256 balanceToDistribute = postSwapBalance - preSwapBalance; + + uint256 totalPendingSwap = projectTaxPendingSwap; + + uint256 projectBalanceToDistribute = (balanceToDistribute * + projectTaxPendingSwap) / totalPendingSwap; + + // We will not have swapped all tax tokens IF the amount was greater than the max auto swap. + // We therefore cannot just set the pending swap counters to 0. Instead, in this scenario, + // we must reduce them in proportion to the swap amount vs the remaining balance + swap + // amount. + // + // For example: + // * swap Balance is 250 + // * contract balance is 385. + // * projectTaxPendingSwap is 300 + // + // The new total for the projectTaxPendingSwap is: + // = 300 - ((300 * 250) / 385) + // = 300 - 194 + // = 106 + + if (swapBalance_ < contractBalance_) { + projectTaxPendingSwap -= uint128( + (projectTaxPendingSwap * swapBalance_) / contractBalance_ + ); + } else { + projectTaxPendingSwap = 0; + } + // Distribute to treasuries: + bool success; + address weth; + uint256 gas; + + if (projectBalanceToDistribute > 0) { + // If no gas limit was provided or provided gas limit greater than gas left, just use the remaining gas. + gas = (CALL_GAS_LIMIT == 0 || CALL_GAS_LIMIT > gasleft()) + ? gasleft() + : CALL_GAS_LIMIT; + + // We limit the gas passed so that a called address cannot cause a block out of gas error: + IERC20(pairToken).transfer{gas: gas}( + projectTaxRecipient, + projectBalanceToDistribute + ); + } + } catch { + // Dont allow a failed external call (in this case to uniswap) to stop a transfer. + // Emit that this has occured and continue. + emit ExternalCallError(5); + } + } + + /** + * @dev distributeTaxTokens + * + * Allows the distribution of tax tokens to the designated recipient(s) + * + * As part of standard processing the tax token balance being above the threshold + * will trigger an autoswap to ETH and distribution of this ETH to the designated + * recipients. This is automatic and there is no need for user involvement. + * + * As part of this swap there are a number of calculations performed, particularly + * if the tax balance is above MAX_SWAP_THRESHOLD_MULTIPLE. + * + * Testing indicates that these calculations are safe. But given the data / code + * interactions it remains possible that some edge case set of scenarios may cause + * an issue with these calculations. + * + * This method is therefore provided as a 'fallback' option to safely distribute + * accumulated taxes from the contract, with a direct transfer of the ERC20 tokens + * themselves. + */ + function distributeTaxTokens() external { + if (projectTaxPendingSwap > 0) { + uint256 projectDistribution = projectTaxPendingSwap; + projectTaxPendingSwap = 0; + _transfer( + address(this), + projectTaxRecipient, + projectDistribution, + false + ); + } + } + + /** + * @dev function {withdrawETH} onlyOwner + * + * A withdraw function to allow ETH to be withdrawn by the manager + * + * This contract should never hold ETH. The only envisaged scenario where + * it might hold ETH is a failed autoswap where the uniswap swap has completed, + * the recipient of ETH reverts, the contract then wraps to WETH and the + * wrap to WETH fails. + * + * This feels unlikely. But, for safety, we include this method. + * + * @param amount_ The amount to withdraw + */ + function withdrawETH(uint256 amount_) external onlyOwner { + (bool success, ) = _msgSender().call{value: amount_}(""); + if (!success) { + revert TransferFailed(); + } + } + + /** + * @dev function {withdrawERC20} onlyOwner + * + * A withdraw function to allow ERC20s (except address(this)) to be withdrawn. + * + * This contract should never hold ERC20s other than tax tokens. The only envisaged + * scenario where it might hold an ERC20 is a failed autoswap where the uniswap swap + * has completed, the recipient of ETH reverts, the contract then wraps to WETH, the + * wrap to WETH succeeds, BUT then the transfer of WETH fails. + * + * This feels even less likely than the scenario where ETH is held on the contract. + * But, for safety, we include this method. + * + * @param token_ The ERC20 contract + * @param amount_ The amount to withdraw + */ + function withdrawERC20(address token_, uint256 amount_) external onlyOwner { + if (token_ == address(this)) { + revert CannotWithdrawThisToken(); + } + IERC20(token_).safeTransfer(_msgSender(), amount_); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + if (account == address(0)) { + revert MintToZeroAddress(); + } + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += uint128(amount); + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + if (account == address(0)) { + revert BurnFromTheZeroAddress(); + } + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + if (accountBalance < amount) { + revert BurnExceedsBalance(); + } + + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= uint128(amount); + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + if (owner == address(0)) { + revert ApproveFromTheZeroAddress(); + } + + if (spender == address(0)) { + revert ApproveToTheZeroAddress(); + } + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + if (currentAllowance < amount) { + revert InsufficientAllowance(); + } + + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Destroys a `value` amount of tokens from the caller. + * + * See {ERC20-_burn}. + */ + function burn(uint256 value) public virtual { + _burn(_msgSender(), value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, deducting from + * the caller's allowance. + * + * See {ERC20-_burn} and {ERC20-allowance}. + * + * Requirements: + * + * - the caller must have allowance for ``accounts``'s tokens of at least + * `value`. + */ + function burnFrom(address account, uint256 value) public virtual { + _spendAllowance(account, _msgSender(), value); + _burn(account, value); + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + receive() external payable {} + function mint(address account, uint256 value) public onlyMinter { - super._mint(account, value); + _mint(account, value); } } diff --git a/contracts/virtualPersona/IAgentNft.sol b/contracts/virtualPersona/IAgentNft.sol index c13563d..6f4fa1d 100644 --- a/contracts/virtualPersona/IAgentNft.sol +++ b/contracts/virtualPersona/IAgentNft.sol @@ -11,6 +11,7 @@ interface IAgentNft is IValidatorRegistry { address tba; // Token Bound Address uint8[] coreTypes; address pool; // Liquidity pool for the agent + address veToken; // Voting escrow token } event CoresUpdated(uint256 virtualId, uint8[] coreTypes); @@ -21,7 +22,9 @@ interface IAgentNft is IValidatorRegistry { string memory newTokenURI, address payable theDAO, address founder, - uint8[] memory coreTypes + uint8[] memory coreTypes, + address pool, + address token ) external returns (uint256); function stakingTokenToVirtualId( diff --git a/contracts/virtualPersona/IAgentToken.sol b/contracts/virtualPersona/IAgentToken.sol index 6c377d2..2d81b5f 100644 --- a/contracts/virtualPersona/IAgentToken.sol +++ b/contracts/virtualPersona/IAgentToken.sol @@ -1,11 +1,362 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -interface IAgentToken { +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./IERC20Config.sol"; +import "./IConfigStructures.sol"; +import "./IErrors.sol"; + +interface IAgentToken is + IConfigStructures, + IERC20, + IERC20Config, + IERC20Metadata, + IErrors +{ + event AutoSwapThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); + + event ExternalCallError(uint256 identifier); + + event InitialLiquidityAdded( + uint256 tokenA, + uint256 tokenB, + uint256 lpToken + ); + + event LimitsUpdated( + uint256 oldMaxTokensPerTransaction, + uint256 newMaxTokensPerTransaction, + uint256 oldMaxTokensPerWallet, + uint256 newMaxTokensPerWallet + ); + + event LiquidityPoolCreated(address addedPool); + + event LiquidityPoolAdded(address addedPool); + + event LiquidityPoolRemoved(address removedPool); + + event ProjectTaxBasisPointsChanged( + uint256 oldBuyBasisPoints, + uint256 newBuyBasisPoints, + uint256 oldSellBasisPoints, + uint256 newSellBasisPoints + ); + + event RevenueAutoSwap(); + + event ProjectTaxRecipientUpdated(address treasury); + + event UnlimitedAddressAdded(address addedUnlimted); + + event UnlimitedAddressRemoved(address removedUnlimted); + + event ValidCallerAdded(bytes32 addedValidCaller); + + event ValidCallerRemoved(bytes32 removedValidCaller); + + /** + * @dev function {addInitialLiquidity} + * + * Add initial liquidity to the uniswap pair + * + * @param lpOwner The recipient of LP tokens + */ + function addInitialLiquidity(address lpOwner) external; + + /** + * @dev function {isLiquidityPool} + * + * Return if an address is a liquidity pool + * + * @param queryAddress_ The address being queried + * @return bool The address is / isn't a liquidity pool + */ + function isLiquidityPool( + address queryAddress_ + ) external view returns (bool); + + /** + * @dev function {liquidityPools} + * + * Returns a list of all liquidity pools + * + * @return liquidityPools_ a list of all liquidity pools + */ + function liquidityPools() + external + view + returns (address[] memory liquidityPools_); + + /** + * @dev function {addLiquidityPool} onlyOwner + * + * Allows the manager to add a liquidity pool to the pool enumerable set + * + * @param newLiquidityPool_ The address of the new liquidity pool + */ + function addLiquidityPool(address newLiquidityPool_) external; + + /** + * @dev function {removeLiquidityPool} onlyOwner + * + * Allows the manager to remove a liquidity pool + * + * @param removedLiquidityPool_ The address of the old removed liquidity pool + */ + function removeLiquidityPool(address removedLiquidityPool_) external; + + /** + * @dev function {isUnlimited} + * + * Return if an address is unlimited (is not subject to per txn and per wallet limits) + * + * @param queryAddress_ The address being queried + * @return bool The address is / isn't unlimited + */ + function isUnlimited(address queryAddress_) external view returns (bool); + + /** + * @dev function {unlimitedAddresses} + * + * Returns a list of all unlimited addresses + * + * @return unlimitedAddresses_ a list of all unlimited addresses + */ + function unlimitedAddresses() + external + view + returns (address[] memory unlimitedAddresses_); + + /** + * @dev function {addUnlimited} onlyOwner + * + * Allows the manager to add an unlimited address + * + * @param newUnlimited_ The address of the new unlimited address + */ + function addUnlimited(address newUnlimited_) external; + + /** + * @dev function {removeUnlimited} onlyOwner + * + * Allows the manager to remove an unlimited address + * + * @param removedUnlimited_ The address of the old removed unlimited address + */ + function removeUnlimited(address removedUnlimited_) external; + + /** + * @dev function {isValidCaller} + * + * Return if an address is a valid caller + * + * @param queryHash_ The code hash being queried + * @return bool The address is / isn't a valid caller + */ + function isValidCaller(bytes32 queryHash_) external view returns (bool); + + /** + * @dev function {validCallers} + * + * Returns a list of all valid caller code hashes + * + * @return validCallerHashes_ a list of all valid caller code hashes + */ + function validCallers() + external + view + returns (bytes32[] memory validCallerHashes_); + + /** + * @dev function {addValidCaller} onlyOwner + * + * Allows the owner to add the hash of a valid caller + * + * @param newValidCallerHash_ The hash of the new valid caller + */ + function addValidCaller(bytes32 newValidCallerHash_) external; + + /** + * @dev function {removeValidCaller} onlyOwner + * + * Allows the owner to remove a valid caller + * + * @param removedValidCallerHash_ The hash of the old removed valid caller + */ + function removeValidCaller(bytes32 removedValidCallerHash_) external; + + /** + * @dev function {setProjectTaxRecipient} onlyOwner + * + * Allows the manager to set the project tax recipient address + * + * @param projectTaxRecipient_ New recipient address + */ + function setProjectTaxRecipient(address projectTaxRecipient_) external; + + /** + * @dev function {setSwapThresholdBasisPoints} onlyOwner + * + * Allows the manager to set the autoswap threshold + * + * @param swapThresholdBasisPoints_ New swap threshold in basis points + */ + function setSwapThresholdBasisPoints( + uint16 swapThresholdBasisPoints_ + ) external; + + /** + * @dev function {setProjectTaxRates} onlyOwner + * + * Change the tax rates, subject to only ever decreasing + * + * @param newProjectBuyTaxBasisPoints_ The new buy tax rate + * @param newProjectSellTaxBasisPoints_ The new sell tax rate + */ + function setProjectTaxRates( + uint16 newProjectBuyTaxBasisPoints_, + uint16 newProjectSellTaxBasisPoints_ + ) external; + + /** + * @dev function {setLimits} onlyOwner + * + * Change the limits on transactions and holdings + * + * @param newMaxTokensPerTransaction_ The new per txn limit + * @param newMaxTokensPerWallet_ The new tokens per wallet limit + */ + function setLimits( + uint256 newMaxTokensPerTransaction_, + uint256 newMaxTokensPerWallet_ + ) external; + + /** + * @dev function {limitsEnforced} + * + * Return if limits are enforced on this contract + * + * @return bool : they are / aren't + */ + function limitsEnforced() external view returns (bool); + + /** + * @dev totalBuyTaxBasisPoints + * + * Provide easy to view tax total: + */ + function totalBuyTaxBasisPoints() external view returns (uint256); + + /** + * @dev totalSellTaxBasisPoints + * + * Provide easy to view tax total: + */ + function totalSellTaxBasisPoints() external view returns (uint256); + + /** + * @dev distributeTaxTokens + * + * Allows the distribution of tax tokens to the designated recipient(s) + * + * As part of standard processing the tax token balance being above the threshold + * will trigger an autoswap to ETH and distribution of this ETH to the designated + * recipients. This is automatic and there is no need for user involvement. + * + * As part of this swap there are a number of calculations performed, particularly + * if the tax balance is above MAX_SWAP_THRESHOLD_MULTIPLE. + * + * Testing indicates that these calculations are safe. But given the data / code + * interactions it remains possible that some edge case set of scenarios may cause + * an issue with these calculations. + * + * This method is therefore provided as a 'fallback' option to safely distribute + * accumulated taxes from the contract, with a direct transfer of the ERC20 tokens + * themselves. + */ + function distributeTaxTokens() external; + + /** + * @dev function {withdrawETH} onlyOwner + * + * A withdraw function to allow ETH to be withdrawn by the manager + * + * This contract should never hold ETH. The only envisaged scenario where + * it might hold ETH is a failed autoswap where the uniswap swap has completed, + * the recipient of ETH reverts, the contract then wraps to WETH and the + * wrap to WETH fails. + * + * This feels unlikely. But, for safety, we include this method. + * + * @param amount_ The amount to withdraw + */ + function withdrawETH(uint256 amount_) external; + + /** + * @dev function {withdrawERC20} onlyOwner + * + * A withdraw function to allow ERC20s (except address(this)) to be withdrawn. + * + * This contract should never hold ERC20s other than tax tokens. The only envisaged + * scenario where it might hold an ERC20 is a failed autoswap where the uniswap swap + * has completed, the recipient of ETH reverts, the contract then wraps to WETH, the + * wrap to WETH succeeds, BUT then the transfer of WETH fails. + * + * This feels even less likely than the scenario where ETH is held on the contract. + * But, for safety, we include this method. + * + * @param token_ The ERC20 contract + * @param amount_ The amount to withdraw + */ + function withdrawERC20(address token_, uint256 amount_) external; + + /** + * @dev Destroys a `value` amount of tokens from the caller. + * + * See {ERC20-_burn}. + */ + function burn(uint256 value) external; + + /** + * @dev Destroys a `value` amount of tokens from `account`, deducting from + * the caller's allowance. + * + * See {ERC20-_burn} and {ERC20-allowance}. + * + * Requirements: + * + * - the caller must have allowance for ``accounts``'s tokens of at least + * `value`. + */ + function burnFrom(address account, uint256 value) external; + + /** + * @dev {initializer} + * + * @param integrationAddresses_ The project owner, uniswap router, LP currency + * @param baseParams_ configuration of this ERC20. + * param supplyParams_ Supply configuration of this ERC20. + * param taxParams_ Tax configuration of this ERC20 + * param taxParams_ Launch pool configuration of this ERC20 + * param lpSupply_ Initial supply to be minted for LP + */ function initialize( - string memory name, - string memory symbol + address[3] memory integrationAddresses_, + bytes memory baseParams_, + bytes memory supplyParams_, + bytes memory taxParams_, + uint256 lpSupply_ ) external; + /** + * @dev {mint} + * + * Allow minter to mint tokens on contribution + * + * @param account The recipient + * @param value mint amount + */ function mint(address account, uint256 value) external; } diff --git a/contracts/virtualPersona/IConfigStructures.sol b/contracts/virtualPersona/IConfigStructures.sol new file mode 100644 index 0000000..7be9c65 --- /dev/null +++ b/contracts/virtualPersona/IConfigStructures.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IConfigStructures { + enum DropStatus { + approved, + deployed, + cancelled + } + + enum TemplateStatus { + live, + terminated + } + + // The current status of the mint: + // - notEnabled: This type of mint is not part of this drop + // - notYetOpen: This type of mint is part of the drop, but it hasn't started yet + // - open: it's ready for ya, get in there. + // - finished: been and gone. + // - unknown: theoretically impossible. + enum MintStatus { + notEnabled, + notYetOpen, + open, + finished, + unknown + } + + struct SubListConfig { + uint256 start; + uint256 end; + uint256 phaseMaxSupply; + } + + struct PrimarySaleModuleInstance { + address instanceAddress; + string instanceDescription; + } + + struct NFTModuleConfig { + uint256 templateId; + bytes configData; + bytes vestingData; + } + + struct PrimarySaleModuleConfig { + uint256 templateId; + bytes configData; + } + + struct ProjectBeneficiary { + address payable payeeAddress; + uint256 payeeShares; + } + + struct VestingConfig { + uint256 start; + uint256 projectUpFrontShare; + uint256 projectVestedShare; + uint256 vestingPeriodInDays; + uint256 vestingCliff; + ProjectBeneficiary[] projectPayees; + } + + struct RoyaltySplitterModuleConfig { + uint256 templateId; + bytes configData; + } + + struct InLifeModuleConfig { + uint256 templateId; + bytes configData; + } + + struct InLifeModules { + InLifeModuleConfig[] modules; + } + + struct NFTConfig { + uint256 supply; + string name; + string symbol; + bytes32 positionProof; + bool includePriorPhasesInMintTracking; + bool singleMetadataCollection; + uint256 reservedAllocation; + uint256 assistanceRequestWindowInSeconds; + } + + struct Template { + TemplateStatus status; + uint16 templateNumber; + uint32 loadedDate; + address payable templateAddress; + string templateDescription; + } + + struct RoyaltyDetails { + address newRoyaltyPaymentSplitterInstance; + uint96 royaltyFromSalesInBasisPoints; + } + + struct SignedDropMessageDetails { + uint256 messageTimeStamp; + bytes32 messageHash; + bytes messageSignature; + } +} \ No newline at end of file diff --git a/contracts/virtualPersona/IERC20Config.sol b/contracts/virtualPersona/IERC20Config.sol new file mode 100644 index 0000000..64fa0c8 --- /dev/null +++ b/contracts/virtualPersona/IERC20Config.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IERC20Config { + struct ERC20Config { + bytes baseParameters; + bytes supplyParameters; + bytes taxParameters; + bytes poolParameters; + } + + struct ERC20BaseParameters { + string name; + string symbol; + } + + struct ERC20SupplyParameters { + uint256 maxTokensPerWallet; + uint256 maxTokensPerTxn; + uint256 botProtectionDurationInSeconds; + } + + struct ERC20TaxParameters { + uint256 projectBuyTaxBasisPoints; + uint256 projectSellTaxBasisPoints; + uint256 taxSwapThresholdBasisPoints; + address projectTaxRecipient; + } +} \ No newline at end of file diff --git a/contracts/virtualPersona/IErrors.sol b/contracts/virtualPersona/IErrors.sol new file mode 100644 index 0000000..6221cb0 --- /dev/null +++ b/contracts/virtualPersona/IErrors.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IErrors { + enum BondingCurveErrorType { + OK, // No error + INVALID_NUMITEMS, // The numItem value is 0 + SPOT_PRICE_OVERFLOW // The updated spot price doesn't fit into 128 bits + } + + error AdapterParamsMustBeEmpty(); // The adapter parameters on this LZ call must be empty. + + error AdditionToPoolIsBelowPerTransactionMinimum(); // The contribution amount is less than the minimum. + + error AdditionToPoolWouldExceedPoolCap(); // This addition to the pool would exceed the pool cap. + + error AdditionToPoolWouldExceedPerAddressCap(); // This addition to the pool would exceed the per address cap. + + error AddressAlreadySet(); // The address being set can only be set once, and is already non-0. + + error AllowanceDecreasedBelowZero(); // You cannot decrease the allowance below zero. + + error AlreadyInitialised(); // The contract is already initialised: it cannot be initialised twice! + + error ApprovalCallerNotOwnerNorApproved(); // The caller must own the token or be an approved operator. + + error ApproveFromTheZeroAddress(); // Approval cannot be called from the zero address (indeed, how have you??). + + error ApproveToTheZeroAddress(); // Approval cannot be given to the zero address. + + error ApprovalQueryForNonexistentToken(); // The token does not exist. + + error AuctionStatusIsNotEnded(); // Throw if the action required the auction to be closed, and it isn't. + + error AuctionStatusIsNotOpen(); // Throw if the action requires the auction to be open, and it isn't. + + error AuxCallFailed( + address[] modules, + uint256 value, + bytes data, + uint256 txGas + ); // An auxilliary call from the drop factory failed. + + error BalanceMismatch(); // An error when comparing balance amounts. + + error BalanceQueryForZeroAddress(); // Cannot query the balance for the zero address. + + error BidMustBeBelowTheFloorWhenReducingQuantity(); // Only bids that are below the floor can reduce the quantity of the bid. + + error BidMustBeBelowTheFloorForRefundDuringAuction(); // Only bids that are below the floor can be refunded during the auction. + + error BondingCurveError(BondingCurveErrorType error); // An error of the type specified has occured in bonding curve processing. + + error BurnExceedsBalance(); // The amount you have selected to burn exceeds the addresses balance. + + error BurnFromTheZeroAddress(); // Tokens cannot be burned from the zero address. (Also, how have you called this!?!) + + error CallerIsNotDepositBoxOwner(); // The caller is not the owner of the deposit box. + + error CallerIsNotFactory(); // The caller of this function must match the factory address in storage. + + error CallerIsNotFactoryOrProjectOwner(); // The caller of this function must match the factory address OR project owner address. + + error CallerIsNotFactoryProjectOwnerOrPool(); // The caller of this function must match the factory address, project owner or pool address. + + error CallerIsNotTheOwner(); // The caller is not the owner of this contract. + + error CallerIsNotTheManager(); // The caller is not the manager of this contract. + + error CallerMustBeLzApp(); // The caller must be an LZ application. + + error CallerIsNotPlatformAdmin(address caller); // The caller of this function must be part of the platformAdmin group. + + error CallerIsNotSuperAdmin(address caller); // The caller of this function must match the superAdmin address in storage. + + error CannotAddLiquidityOnCreateAndUseDRIPool(); // Cannot use both liquidity added on create and a DRIPool in the same token. + + error CannotSetNewOwnerToTheZeroAddress(); // You can't set the owner of this contract to the zero address (address(0)). + + error CannotSetToZeroAddress(); // The corresponding address cannot be set to the zero address (address(0)). + + error CannotSetNewManagerToTheZeroAddress(); // Cannot transfer the manager to the zero address (address(0)). + + error CannotWithdrawThisToken(); // Cannot withdraw the specified token. + + error CanOnlyReduce(); // The given operation can only reduce the value specified. + + error CollectionAlreadyRevealed(); // The collection is already revealed; you cannot call reveal again. + + error ContractIsDecommissioned(); // This contract is decommissioned! + + error ContractIsPaused(); // The call requires the contract to be unpaused, and it is paused. + + error ContractIsNotPaused(); // The call required the contract to be paused, and it is NOT paused. + + error DecreasedAllowanceBelowZero(); // The request would decrease the allowance below zero, and that is not allowed. + + error DestinationIsNotTrustedSource(); // The destination that is being called through LZ has not been set as trusted. + + error DeployerOnly(); // This method can only be called by the deployer address. + + error DeploymentError(); // Error on deployment. + + error DepositBoxIsNotOpen(); // This action cannot complete as the deposit box is not open. + + error DriPoolAddressCannotBeAddressZero(); // The Dri Pool address cannot be the zero address. + + error GasLimitIsTooLow(); // The gas limit for the LayerZero call is too low. + + error IncorrectConfirmationValue(); // You need to enter the right confirmation value to call this funtion (usually 69420). + + error IncorrectPayment(); // The function call did not include passing the correct payment. + + error InitialLiquidityAlreadyAdded(); // Initial liquidity has already been added. You can't do it again. + + error InitialLiquidityNotYetAdded(); // Initial liquidity needs to have been added for this to succedd. + + error InsufficientAllowance(); // There is not a high enough allowance for this operation. + + error InvalidAdapterParams(); // The current adapter params for LayerZero on this contract won't work :(. + + error InvalidAddress(); // An address being processed in the function is not valid. + + error InvalidEndpointCaller(); // The calling address is not a valid LZ endpoint. The LZ endpoint was set at contract creation + // and cannot be altered after. Check the address LZ endpoint address on the contract. + + error InvalidMinGas(); // The minimum gas setting for LZ in invalid. + + error InvalidOracleSignature(); // The signature provided with the contract call is not valid, either in format or signer. + + error InvalidPayload(); // The LZ payload is invalid + + error InvalidReceiver(); // The address used as a target for funds is not valid. + + error InvalidSourceSendingContract(); // The LZ message is being related from a source contract on another chain that is NOT trusted. + + error InvalidTotalShares(); // Total shares must equal 100 percent in basis points. + + error LimitsCanOnlyBeRaised(); // Limits are UP ONLY. + + error ListLengthMismatch(); // Two or more lists were compared and they did not match length. + + error LiquidityPoolMustBeAContractAddress(); // Cannot add a non-contract as a liquidity pool. + + error LiquidityPoolCannotBeAddressZero(); // Cannot add a liquidity pool from the zero address. + + error LPLockUpMustFitUint88(); // LP lockup is held in a uint88, so must fit. + + error NoTrustedPathRecord(); // LZ needs a trusted path record for this to work. What's that, you ask? + + error MachineAddressCannotBeAddressZero(); // Cannot set the machine address to the zero address. + + error ManagerUnauthorizedAccount(); // The caller is not the pending manager. + + error MaxBidQuantityIs255(); // Validation: as we use a uint8 array to track bid positions the max bid quantity is 255. + + error MaxPublicMintAllowanceExceeded( + uint256 requested, + uint256 alreadyMinted, + uint256 maxAllowance + ); // The calling address has requested a quantity that would exceed the max allowance. + + error MaxSupplyTooHigh(); // Max supply must fit in a uint128. + + error MaxTokensPerWalletExceeded(); // The transfer would exceed the max tokens per wallet limit. + + error MaxTokensPerTxnExceeded(); // The transfer would exceed the max tokens per transaction limit. + + error MetadataIsLocked(); // The metadata on this contract is locked; it cannot be altered! + + error MinGasLimitNotSet(); // The minimum gas limit for LayerZero has not been set. + + error MintERC2309QuantityExceedsLimit(); // The `quantity` minted with ERC2309 exceeds the safety limit. + + error MintingIsClosedForever(); // Minting is, as the error suggests, so over (and locked forever). + + error MintToZeroAddress(); // Cannot mint to the zero address. + + error MintZeroQuantity(); // The quantity of tokens minted must be more than zero. + + error NewBuyTaxBasisPointsExceedsMaximum(); // Project owner trying to set the tax rate too high. + + error NewSellTaxBasisPointsExceedsMaximum(); // Project owner trying to set the tax rate too high. + + error NoETHForLiquidityPair(); // No ETH has been provided for the liquidity pair. + + error TaxPeriodStillInForce(); // The minimum tax period has not yet expired. + + error NoPaymentDue(); // No payment is due for this address. + + error NoRefundForCaller(); // Error thrown when the calling address has no refund owed. + + error NoStoredMessage(); // There is no stored message matching the passed parameters. + + error NothingToClaim(); // The calling address has nothing to claim. + + error NoTokenForLiquidityPair(); // There is no token to add to the LP. + + error OperationDidNotSucceed(); // The operation failed (vague much?). + + error OracleSignatureHasExpired(); // A signature has been provided but it is too old. + + error OwnershipNotInitializedForExtraData(); // The `extraData` cannot be set on an uninitialized ownership slot. + + error OwnerQueryForNonexistentToken(); // The token does not exist. + + error ParametersDoNotMatchSignedMessage(); // The parameters passed with the signed message do not match the message itself. + + error ParamTooLargeStartDate(); // The passed parameter exceeds the var type max. + + error ParamTooLargeEndDate(); // The passed parameter exceeds the var type max. + + error ParamTooLargeMinETH(); // The passed parameter exceeds the var type max. + + error ParamTooLargePerAddressMax(); // The passed parameter exceeds the var type max. + + error ParamTooLargeVestingDays(); // The passed parameter exceeds the var type max. + + error ParamTooLargePoolSupply(); // The passed parameter exceeds the var type max. + + error ParamTooLargePoolPerTxnMinETH(); // The passed parameter exceeds the var type max. + + error PassedConfigDoesNotMatchApproved(); // The config provided on the call does not match the approved config. + + error PauseCutOffHasPassed(); // The time period in which we can pause has passed; this contract can no longer be paused. + + error PaymentMustCoverPerMintFee(); // The payment passed must at least cover the per mint fee for the quantity requested. + + error PermitDidNotSucceed(); // The safeERC20 permit failed. + + error PlatformAdminCannotBeAddressZero(); // We cannot use the zero address (address(0)) as a platformAdmin. + + error PlatformTreasuryCannotBeAddressZero(); // The treasury address cannot be set to the zero address. + + error PoolIsAboveMinimum(); // You required the pool to be below the minimum, and it is not + + error PoolIsBelowMinimum(); // You required the pool to be above the minimum, and it is not + + error PoolPhaseIsClosed(); // The block.timestamp is either before the pool is open or after it is closed. + + error PoolPhaseIsNotAfter(); // The block.timestamp is either before or during the pool open phase. + + error PoolVestingNotYetComplete(); // Tokens in the pool are not yet vested. + + error ProjectOwnerCannotBeAddressZero(); // The project owner has to be a non zero address. + + error ProofInvalid(); // The provided proof is not valid with the provided arguments. + + error QuantityExceedsRemainingCollectionSupply(); // The requested quantity would breach the collection supply. + + error QuantityExceedsRemainingPhaseSupply(); // The requested quantity would breach the phase supply. + + error QuantityExceedsMaxPossibleCollectionSupply(); // The requested quantity would breach the maximum trackable supply + + error ReferralIdAlreadyUsed(); // This referral ID has already been used; they are one use only. + + error RequestingMoreThanAvailableBalance(); // The request exceeds the available balance. + + error RequestingMoreThanRemainingAllocation( + uint256 previouslyMinted, + uint256 requested, + uint256 remainingAllocation + ); // Number of tokens requested for this mint exceeds the remaining allocation (taking the + // original allocation from the list and deducting minted tokens). + + error RoyaltyFeeWillExceedSalePrice(); // The ERC2981 royalty specified will exceed the sale price. + + error ShareTotalCannotBeZero(); // The total of all the shares cannot be nothing. + + error SliceOutOfBounds(); // The bytes slice operation was out of bounds. + + error SliceOverflow(); // The bytes slice operation overlowed. + + error SuperAdminCannotBeAddressZero(); // The superAdmin cannot be the sero address (address(0)). + + error SupplyTotalMismatch(); // The sum of the team supply and lp supply does not match. + + error SupportWindowIsNotOpen(); // The project owner has not requested support within the support request expiry window. + + error TaxFreeAddressCannotBeAddressZero(); // A tax free address cannot be address(0) + + error TemplateCannotBeAddressZero(); // The address for a template cannot be address zero (address(0)). + + error TemplateNotFound(); // There is no template that matches the passed template Id. + + error ThisMintIsClosed(); // It's over (well, this mint is, anyway). + + error TotalSharesMustMatchDenominator(); // The total of all shares must equal the denominator value. + + error TransferAmountExceedsBalance(); // The transfer amount exceeds the accounts available balance. + + error TransferCallerNotOwnerNorApproved(); // The caller must own the token or be an approved operator. + + error TransferFailed(); // The transfer has failed. + + error TransferFromIncorrectOwner(); // The token must be owned by `from`. + + error TransferToNonERC721ReceiverImplementer(); // Cannot safely transfer to a contract that does not implement the ERC721Receiver interface. + + error TransferFromZeroAddress(); // Cannot transfer from the zero address. Indeed, this surely is impossible, and likely a waste to check?? + + error TransferToZeroAddress(); // Cannot transfer to the zero address. + + error UnrecognisedVRFMode(); // Currently supported VRF modes are 0: chainlink and 1: arrng + + error URIQueryForNonexistentToken(); // The token does not exist. + + error ValueExceedsMaximum(); // The value sent exceeds the maximum allowed (super useful explanation huh?). + + error VRFCoordinatorCannotBeAddressZero(); // The VRF coordinator cannot be the zero address (address(0)). +} \ No newline at end of file diff --git a/test/genesisDAO.js b/test/deprecated/genesisDAO.js similarity index 100% rename from test/genesisDAO.js rename to test/deprecated/genesisDAO.js diff --git a/test/rewards.js b/test/deprecated/rewards.js similarity index 100% rename from test/rewards.js rename to test/deprecated/rewards.js diff --git a/test/kwtest.js b/test/kwtest.js deleted file mode 100644 index 3afd239..0000000 --- a/test/kwtest.js +++ /dev/null @@ -1,43 +0,0 @@ -/* -We will test the end-to-end implementation of a Contribution flow till Service. - -1. Prepare 100k tokens -2. Propose a new Persona at AgentFactory -3. Once received proposalId from AgentFactory, create a proposal at ProtocolDAO -4. Vote on the proposal -5. Execute the proposal -*/ -const { parseEther, formatEther, toBeHex } = require("ethers/utils"); -const { ethers } = require("hardhat"); -const abi = ethers.AbiCoder.defaultAbiCoder(); -const { expect } = require("chai"); -const { - loadFixture, - mine, -} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); - -const getExecuteCallData = (factory, proposalId) => { - return factory.interface.encodeFunctionData("executeApplication", [ - proposalId, - ]); -}; - -const getMintServiceCalldata = async (serviceNft, virtualId, hash) => { - return serviceNft.interface.encodeFunctionData("mint", [virtualId, hash]); -}; - -describe("KwTest", function () { - - - before(async function () { - const signers = await ethers.getSigners(); - this.accounts = signers.map((signer) => signer.address); - this.signers = signers; - }); - - it("test router", async function () { - console.log(process.env.UNISWAP_ROUTER,) - const c = await ethers.getContractAt("IUniswapV2Router02", "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D") - console.log(await c.factory()) - }); -}); diff --git a/test/rewardsV2.js b/test/rewardsV2.js index 48f0960..e96bf67 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -29,16 +29,9 @@ function getDescHash(str) { } describe("RewardsV2", function () { - const PROPOSAL_THRESHOLD = parseEther("5000"); - const QUORUM = parseEther("10000"); - const STAKE_AMOUNTS = [ - parseEther("5000"), - parseEther("100000"), - parseEther("5000"), - parseEther("2000"), - ]; - const UPTIME = [3, 1]; - const REWARD_AMOUNT = parseEther("2000"); + const PROPOSAL_THRESHOLD = parseEther("100000"); //100k + const MATURITY_SCORE = toBeHex(2000, 32); // 20% + const IP_SHARE = 1000; // 10% const TOKEN_URI = "http://jessica"; @@ -50,170 +43,161 @@ describe("RewardsV2", function () { cores: [0, 1, 2], tbaSalt: "0xa7647ac9429fdce477ebd9a95510385b756c757c26149e740abbab0ad1be2f16", - tbaImplementation: ethers.ZeroAddress, + tbaImplementation: process.env.TBA_IMPLEMENTATION, daoVotingPeriod: 600, daoThreshold: 1000000000000000000000n, }; + const getAccounts = async () => { + const [ + deployer, + ipVault, + founder, + contributor1, + contributor2, + validator1, + validator2, + ] = await ethers.getSigners(); + return { + deployer, + ipVault, + founder, + contributor1, + contributor2, + validator1, + validator2, + }; + }; + async function deployBaseContracts() { - const signers = await ethers.getSigners(); + const { deployer, ipVault } = await getAccounts(); - const [deployer] = await ethers.getSigners(); - const veToken = await ethers.deployContract( - "veVirtualToken", - [deployer.address], + const virtualToken = await ethers.deployContract( + "VirtualToken", + [PROPOSAL_THRESHOLD, deployer.address], {} ); - await veToken.waitForDeployment(); + await virtualToken.waitForDeployment(); - const demoToken = await ethers.deployContract( - "BMWToken", - [deployer.address], + const AgentNft = await ethers.getContractFactory("AgentNft"); + const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); + + const contribution = await upgrades.deployProxy( + await ethers.getContractFactory("ContributionNft"), + [agentNft.target], {} ); - await demoToken.waitForDeployment(); - const protocolDAO = await ethers.deployContract( - "VirtualGenesisDAO", - [veToken.target, 0, 100, 0], + const service = await upgrades.deployProxy( + await ethers.getContractFactory("ServiceNft"), + [agentNft.target, contribution.target, process.env.DATASET_SHARES], {} ); - await protocolDAO.waitForDeployment(); - - const AgentNft = await ethers.getContractFactory("AgentNft"); - const personaNft = await upgrades.deployProxy(AgentNft, [deployer.address]); - const personaToken = await ethers.deployContract("AgentToken"); - await personaToken.waitForDeployment(); - const personaDAO = await ethers.deployContract("AgentDAO"); - await personaDAO.waitForDeployment(); + await agentNft.setContributionService(contribution.target, service.target); - const tba = await ethers.deployContract("ERC6551Registry"); + // Implementation contracts + const agentToken = await ethers.deployContract("AgentToken"); + await agentToken.waitForDeployment(); + const agentDAO = await ethers.deployContract("AgentDAO"); + await agentDAO.waitForDeployment(); + const agentVeToken = await ethers.deployContract("AgentVeToken"); + await agentVeToken.waitForDeployment(); - const personaFactory = await upgrades.deployProxy( - await ethers.getContractFactory("AgentFactory"), + const agentFactory = await upgrades.deployProxy( + await ethers.getContractFactory("AgentFactoryV2"), [ - personaToken.target, - personaDAO.target, - tba.target, - demoToken.target, - personaNft.target, + agentToken.target, + agentVeToken.target, + agentDAO.target, + process.env.TBA_REGISTRY, + virtualToken.target, + agentNft.target, PROPOSAL_THRESHOLD, - 5, - protocolDAO.target, deployer.address, ] ); - - await personaNft.grantRole( - await personaNft.MINTER_ROLE(), - personaFactory.target - ); - - const reward = await ethers.deployContract("AgentRewardX2", [], {}); - await reward.waitForDeployment(); - - const contributionNft = await upgrades.deployProxy( - await ethers.getContractFactory("ContributionNft"), - [personaNft.target], - {} - ); - - const serviceNft = await upgrades.deployProxy( - await ethers.getContractFactory("ServiceNft"), - [personaNft.target, contributionNft.target, process.env.DATASET_SHARES], - {} - ); - - await personaNft.setContributionService( - contributionNft.target, - serviceNft.target - ); - - await reward.initialize( - demoToken.target, - personaNft.target, - contributionNft.target, - serviceNft.target, - { - protocolShares: 1000, - contributorShares: 5000, - stakerShares: 9000, - parentShares: 2000, - stakeThreshold: "1000000000000000000000", - } - ); - const role = await reward.GOV_ROLE(); - await reward.grantRole(role, signers[0].address); + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + const minter = await ethers.deployContract("Minter", [ + service.target, + contribution.target, + agentNft.target, + IP_SHARE, + ipVault.address, + agentFactory.target, + deployer.address, + ]); + await minter.waitForDeployment(); + await agentFactory.setMinter(minter.target); + await agentFactory.setMaturityDuration(86400 * 365 * 10); // 100years + await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); return { - reward, - veToken, - protocolDAO, - demoToken, - personaFactory, - personaNft, - contributionNft, - serviceNft, + virtualToken, + agentFactory, + agentNft, + serviceNft: service, + contributionNft: contribution, + minter, }; } - async function deployGenesisVirtual() { - const contracts = await deployBaseContracts(); - const { personaFactory, veToken, protocolDAO, demoToken } = contracts; - const [deployer] = await ethers.getSigners(); + async function deployWithApplication() { + const base = await deployBaseContracts(); + const { agentFactory, virtualToken } = base; + const { founder } = await getAccounts(); // Prepare tokens for proposal - await demoToken.mint(deployer.address, PROPOSAL_THRESHOLD); - await demoToken.approve(personaFactory.target, PROPOSAL_THRESHOLD); - - await personaFactory.proposePersona( - genesisInput.name, - genesisInput.symbol, - genesisInput.tokenURI, - genesisInput.cores, - genesisInput.tbaSalt, - genesisInput.tbaImplementation, - genesisInput.daoVotingPeriod, - genesisInput.daoThreshold - ); + await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); + await virtualToken + .connect(founder) + .approve(agentFactory.target, PROPOSAL_THRESHOLD); + + const tx = await agentFactory + .connect(founder) + .proposeAgent( + genesisInput.name, + genesisInput.symbol, + genesisInput.tokenURI, + genesisInput.cores, + genesisInput.tbaSalt, + genesisInput.tbaImplementation, + genesisInput.daoVotingPeriod, + genesisInput.daoThreshold + ); - const filter = personaFactory.filters.NewApplication; - const events = await personaFactory.queryFilter(filter, -1); + const filter = agentFactory.filters.NewApplication; + const events = await agentFactory.queryFilter(filter, -1); const event = events[0]; const { id } = event.args; + return { applicationId: id, ...base }; + } - // Create proposal - await veToken.oracleTransfer( - [ethers.ZeroAddress], - [deployer.address], - [parseEther("100000000")] - ); - await veToken.delegate(deployer.address); + async function deployWithAgent() { + const base = await deployWithApplication(); + const { agentFactory, applicationId } = base; - await protocolDAO.propose( - [personaFactory.target], - [0], - [getExecuteCallData(personaFactory, id)], - "Create Jessica" - ); + const { founder } = await getAccounts(); + await agentFactory.connect(founder).executeApplication(applicationId); - const daoFilter = protocolDAO.filters.ProposalCreated; - const daoEvents = await protocolDAO.queryFilter(daoFilter, -1); - const daoEvent = daoEvents[0]; - const daoProposalId = daoEvent.args[0]; - - await protocolDAO.castVote(daoProposalId, 1); - await mine(600); - - await protocolDAO.execute(daoProposalId); - const factoryFilter = personaFactory.filters.NewPersona; - const factoryEvents = await personaFactory.queryFilter(factoryFilter, -1); + const factoryFilter = agentFactory.filters.NewPersona; + const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); const factoryEvent = factoryEvents[0]; - const { virtualId, token, dao } = factoryEvent.args; - const persona = { virtualId, token, dao }; - return { ...contracts, persona }; + const { virtualId, token, veToken, dao, tba, lp } = await factoryEvent.args; + + return { + ...base, + agent: { + virtualId, + token, + veToken, + dao, + tba, + lp, + }, + }; } async function stakeAndVote() { @@ -280,27 +264,27 @@ describe("RewardsV2", function () { base, account ) { - const signers = await ethers.getSigners(); - const { persona, serviceNft, contributionNft } = base; - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + const { founder } = await getAccounts(); + const { agent, serviceNft, contributionNft, minter } = base; + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); const descHash = getDescHash(desc); const mintCalldata = await getMintServiceCalldata( serviceNft, - persona.virtualId, + agent.virtualId, descHash ); - await personaDAO.propose([serviceNft.target], [0], [mintCalldata], desc); - const filter = personaDAO.filters.ProposalCreated; - const events = await personaDAO.queryFilter(filter, -1); + await agentDAO.propose([serviceNft.target], [0], [mintCalldata], desc); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); const event = events[0]; const proposalId = event.args[0]; await contributionNft.mint( account, - persona.virtualId, + agent.virtualId, coreId, TOKEN_URI, proposalId, @@ -312,18 +296,13 @@ describe("RewardsV2", function () { const voteParams = isModel ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) : "0x"; - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 1, - "lfg", - voteParams - ); - await personaDAO - .connect(signers[2]) + await agentDAO + .connect(founder) .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); await mine(600); - await personaDAO.execute(proposalId); + await agentDAO.execute(proposalId); + await minter.mint(proposalId); return proposalId; } @@ -414,295 +393,54 @@ describe("RewardsV2", function () { return { contributionList, ...base }; } - before(async function () { - const signers = await ethers.getSigners(); - this.accounts = signers.map((signer) => signer.address); - this.signers = signers; - }); - - it("should be able to retrieve past reward settings", async function () { - const { reward } = await loadFixture(deployBaseContracts); - const settings = [1000n, 5000n, 9000n, 2000n, 1000000000000000000000n]; - expect(await reward.getRewardSettings()).to.deep.equal(settings); - let blockNumber = await ethers.provider.getBlockNumber(); - expect(await reward.getPastRewardSettings(blockNumber - 1)).to.deep.equal( - settings - ); - for (let i = 0; i < 10; i++) { - const val = i + 2; - let blockNumber = await ethers.provider.getBlockNumber(); - await reward.setRewardSettings(val, val, val, val, val); - await mine(1); - expect(await reward.getPastRewardSettings(blockNumber + 1)).to.deep.equal( - [BigInt(val), BigInt(val), BigInt(val), BigInt(val), BigInt(val)] - ); - } - expect(await reward.getRewardSettings()).to.deep.equal([ - 11n, - 11n, - 11n, - 11n, - 11n, - ]); - }); - - it("should calculate correct staker and validator rewards", async function () { - const { reward, persona } = await loadFixture(stakeAndVote); - const [validator1, staker1, validator2, staker2] = this.accounts; - await mine(1); - - expect( - parseFloat( - formatEther(await reward.getTotalClaimableRewards(validator1, [1], [])) - ).toFixed(4) - ).to.be.equal("60.2679"); - expect( - parseFloat( - formatEther(await reward.getTotalClaimableRewards(staker1, [1], [])) - ).toFixed(4) - ).to.be.equal("361.6071"); - expect( - parseFloat( - formatEther(await reward.getTotalClaimableRewards(validator2, [1], [])) - ).toFixed(4) - ).to.be.equal("41.7857"); - expect( - parseFloat( - formatEther(await reward.getTotalClaimableRewards(staker2, [1], [])) - ).toFixed(4) - ).to.be.equal("14.4643"); - }); - - it("should withdraw correct staker and validator rewards", async function () { - const { reward, demoToken } = await loadFixture(stakeAndVote); - const [validator1, staker1, validator2, staker2] = this.signers; - await mine(1); - - expect(await demoToken.balanceOf(reward.target)).to.be.equal( - parseEther("2000") - ); + before(async function () {}); - expect(await demoToken.balanceOf(validator1.address)).to.be.equal( - parseEther("0") - ); - expect(await demoToken.balanceOf(staker1.address)).to.be.equal( - parseEther("0") - ); - expect(await demoToken.balanceOf(validator2.address)).to.be.equal( - parseEther("0") + it("should mint agent token for successful contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1 } = await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token ); - expect(await demoToken.balanceOf(staker2.address)).to.be.equal( - parseEther("0") + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); + await createContribution( + 0, + maturity, + 0, + true, + 0, + "Test", + base, + contributor1.address ); - - await reward.claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(validator1))).toFixed(4) - ).to.be.equal("60.2679"); - - await reward.connect(staker1).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(staker1))).toFixed(4) - ).to.be.equal("361.6071"); - - await reward.connect(validator2).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(validator2))).toFixed(4) - ).to.be.equal("41.7857"); - - await reward.connect(staker2).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(staker2))).toFixed(4) - ).to.be.equal("14.4643"); - - // Prevent double claim - await reward.claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(validator1))).toFixed(4) - ).to.be.equal("60.2679"); - await expect(reward.connect(staker1).claimAllRewards([1], [])); - expect( - parseFloat(formatEther(await demoToken.balanceOf(staker1))).toFixed(4) - ).to.be.equal("361.6071"); - await expect(reward.connect(validator2).claimAllRewards([1], [])); - expect( - parseFloat(formatEther(await demoToken.balanceOf(validator2))).toFixed(4) - ).to.be.equal("41.7857"); - await expect(reward.connect(staker2).claimAllRewards([1], [])); - expect( - parseFloat(formatEther(await demoToken.balanceOf(staker2))).toFixed(4) - ).to.be.equal("14.4643"); - }); - - it("should calculate correct contributor rewards", async function () { - const { contributionList, demoToken, reward, serviceNft } = - await loadFixture(prepareContributions); - const taxCollector = this.signers[10]; - expect(await serviceNft.getMaturity(contributionList[0])).to.equal(200n); - expect(await serviceNft.getMaturity(contributionList[1])).to.equal(200n); - expect(await serviceNft.getMaturity(contributionList[2])).to.equal(100n); - expect(await serviceNft.getMaturity(contributionList[3])).to.equal(100n); - - expect(await serviceNft.getImpact(contributionList[0])).to.equal(140n); - expect(await serviceNft.getImpact(contributionList[1])).to.equal(60n); - expect(await serviceNft.getImpact(contributionList[2])).to.equal(70n); - expect(await serviceNft.getImpact(contributionList[3])).to.equal(30n); - - expect( - formatEther( - await reward.getTotalClaimableRewards( - taxCollector.address, - [], - [contributionList[0]] - ) - ) - ).to.be.equal("210.0"); - expect( - formatEther( - await reward.getTotalClaimableRewards( - taxCollector.address, - [], - [contributionList[1]] - ) - ) - ).to.be.equal("90.0"); - expect( - formatEther( - await reward.getTotalClaimableRewards( - taxCollector.address, - [], - [contributionList[2]] - ) - ) - ).to.be.equal("210.0"); - expect( - formatEther( - await reward.getTotalClaimableRewards( - taxCollector.address, - [], - [contributionList[3]] - ) - ) - ).to.be.equal("90.0"); - expect( - formatEther( - await reward.getTotalClaimableRewards( - taxCollector.address, - [], - [contributionList[4]] - ) - ) - ).to.be.equal("300.0"); - }); - - it("should claim correct model contributor rewards", async function () { - const { contributionList, reward, serviceNft, demoToken } = - await loadFixture(prepareContributions); - - const taxCollector = this.signers[10]; - expect(await demoToken.balanceOf(taxCollector.address)).to.be.equal(0); - - await expect( - reward.connect(taxCollector).claimAllRewards([], [contributionList[0]]) - ).to.be.fulfilled; - expect( - formatEther(await demoToken.balanceOf(taxCollector.address)) - ).to.be.equal("210.0"); - await expect( - reward.connect(taxCollector).claimAllRewards([], [contributionList[1]]) - ).to.be.fulfilled; - expect( - formatEther(await demoToken.balanceOf(taxCollector.address)) - ).to.be.equal("300.0"); - await expect( - reward.connect(taxCollector).claimAllRewards([], [contributionList[2]]) - ).to.be.fulfilled; - expect( - formatEther(await demoToken.balanceOf(taxCollector.address)) - ).to.be.equal("510.0"); - await expect( - reward.connect(taxCollector).claimAllRewards([], [contributionList[3]]) - ).to.be.fulfilled; - expect( - formatEther(await demoToken.balanceOf(taxCollector.address)) - ).to.be.equal("600.0"); - await expect( - reward.connect(taxCollector).claimAllRewards([], [contributionList[4]]) - ).to.be.fulfilled; - expect( - formatEther(await demoToken.balanceOf(taxCollector.address)) - ).to.be.equal("900.0"); - - // Prevent double claim - await expect( - reward.connect(taxCollector).claimAllRewards([], contributionList) - ).to.be.fulfilled; - expect( - formatEther(await demoToken.balanceOf(taxCollector.address)) - ).to.be.equal("900.0"); - }); - - it("should claim correct total rewards", async function () { - const { contributionList, reward, serviceNft, demoToken } = - await loadFixture(prepareContributions); - const [validator1, staker1, validator2, staker2] = this.signers; - const taxCollector = this.signers[10]; - await mine(1); - - await reward.connect(validator1).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(validator1))).toFixed(4) - ).to.be.equal("163.5842"); - await reward.connect(staker1).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(staker1))).toFixed(4) - ).to.be.equal("981.5051"); - await reward.connect(validator2).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(validator2))).toFixed(4) - ).to.be.equal("83.5714"); - await reward.connect(staker2).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(staker2))).toFixed(4) - ).to.be.equal("28.9286"); - - await expect( - reward.connect(taxCollector).claimAllRewards([], contributionList) - ).to.be.fulfilled; - expect( - parseFloat(formatEther(await demoToken.balanceOf(taxCollector))).toFixed( - 4 - ) - ).to.be.equal("900.0000"); + const balance2 = await agentToken.balanceOf(contributor1.address); + expect(balance2).to.equal(parseEther(maturity.toString())); }); - it("should withdraw correct protocol rewards", async function () { - const { contributionList, reward, serviceNft, demoToken } = - await loadFixture(prepareContributions); - const treasury = this.signers[9]; - - expect(await demoToken.balanceOf(treasury.address)).to.be.equal( - parseEther("0") + it("should mint agent token for IP owner on successful contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1 } = await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token ); - await reward.withdrawProtocolRewards(treasury.address); - expect(await demoToken.balanceOf(treasury.address)).to.be.equal( - parseEther("400") + const balance1 = await agentToken.balanceOf(ipVault.address); + expect(balance1).to.equal(0n); + await createContribution( + 0, + maturity, + 0, + true, + 0, + "Test", + base, + contributor1.address ); - }); - it("should withdraw correct validator pool rewards", async function () { - const { contributionList, reward, serviceNft, demoToken } = - await loadFixture(prepareContributions); - const treasury = this.signers[9]; - - expect(await demoToken.balanceOf(treasury.address)).to.be.equal( - parseEther("0") - ); - await reward.withdrawValidatorPoolRewards(treasury.address); - expect( - parseFloat( - formatEther(await demoToken.balanceOf(treasury.address)) - ).toFixed(4) - ).to.be.equal("542.4107"); + const balance2 = await agentToken.balanceOf(ipVault.address); + expect(balance2).to.equal(parseEther((maturity * 0.1).toString())); }); }); From 2ea25708d0bd24fd4ecba49c0304bb059e1798f7 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 5 Jun 2024 18:20:18 +0800 Subject: [PATCH 10/30] done testing auto swap tax --- contracts/virtualPersona/AgentDAO.sol | 15 +- contracts/virtualPersona/AgentFactory.sol | 55 ++- contracts/virtualPersona/AgentToken.sol | 122 ++++--- contracts/virtualPersona/IErrors.sol | 2 + test/agentDAO.js | 393 ++++++++++++++++++++++ test/rewardsV2.js | 18 +- 6 files changed, 509 insertions(+), 96 deletions(-) create mode 100644 test/agentDAO.js diff --git a/contracts/virtualPersona/AgentDAO.sol b/contracts/virtualPersona/AgentDAO.sol index ee760a5..e3e8442 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -11,7 +11,6 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "../contribution/IContributionNft.sol"; - contract AgentDAO is IAgentDAO, GovernorSettingsUpgradeable, @@ -222,4 +221,18 @@ contract AgentDAO is function quorumDenominator() public pure override returns (uint256) { return 10000; } + + function state( + uint256 proposalId + ) public view override(GovernorUpgradeable) returns (ProposalState) { + // Allow early execution when reached 100% for votes + ProposalState currentState = super.state(proposalId); + if (currentState == ProposalState.Active) { + (, uint256 forVotes, ) = proposalVotes(proposalId); + if (forVotes == token().getPastTotalSupply(proposalSnapshot(proposalId))) { + return ProposalState.Succeeded; + } + } + return currentState; + } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index 3ae5f19..39b50a8 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -99,11 +99,12 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { address public veTokenImplementation; address private _minter; address private _tokenAdmin; + address public defaultDelegatee; // Default agent token params bytes private _tokenSupplyParams; bytes private _tokenTaxParams; - uint16 private _tokenMultiplier = 10000; + uint16 private _tokenMultiplier; /////////////////////////////////////////////////////////////// @@ -132,6 +133,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { _nextId = 1; _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _vault = vault_; + _tokenMultiplier = 10000; } function getApplication( @@ -224,14 +226,12 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { function executeApplication(uint256 id) public noReentrant { // This will bootstrap an Agent with following components: // C1: Agent Token - // C2: LP Pool + // C2: LP Pool + Initial liquidity // C3: Agent veToken // C4: Agent DAO // C5: Agent NFT // C6: TBA - // C7: Mint initial Agent tokens - // C8: Provide liquidity - // C9: Stake liquidity token to get veToken + // C7: Stake liquidity token to get veToken require( _applications[id].status == ApplicationStatus.Active, "Application is not active" @@ -247,8 +247,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { "Not proposer" ); - uint256 initialSupply = (application.withdrawableAmount * - _tokenMultiplier) / 10000; // Initial LP supply + uint256 initialAmount = application.withdrawableAmount; + uint256 initialSupply = (initialAmount * _tokenMultiplier) / 10000; // Initial LP supply application.withdrawableAmount = 0; application.status = ApplicationStatus.Executed; @@ -259,7 +259,11 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { application.symbol, initialSupply ); + + // C2 address lp = IAgentToken(token).liquidityPools()[0]; + IERC20(assetToken).transfer(token, initialAmount); + IAgentToken(token).addInitialLiquidity(address(this)); // C3 address veToken = _createNewAgentVeToken( @@ -268,6 +272,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { lp, application.proposer ); + // C4 string memory daoName = string.concat(application.name, " DAO"); @@ -308,28 +313,14 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); IAgentNft(nft).setTBA(virtualId, tbaAddress); - // C8 - /*IERC20(assetToken).approve(address(_uniswapRouter), initialSupply); - IERC20(token).approve(address(_uniswapRouter), initialSupply); - (, , uint liquidity) = _uniswapRouter.addLiquidity( - token, - assetToken, - initialSupply, - initialSupply, - 0, - 0, - address(this), - block.timestamp + 60 - );*/ - - // C9 - // IERC20(lp).approve(veToken, liquidity); - // IAgentVeToken(veToken).stake( - // liquidity, - // application.proposer, - // application.proposer - // ); - + // C7 + IERC20(lp).approve(veToken, type(uint256).max); + IAgentVeToken(veToken).stake( + IERC20(lp).balanceOf(address(this)), + application.proposer, + defaultDelegatee + ); + emit NewPersona(virtualId, token, veToken, dao, tbaAddress, lp); } @@ -470,4 +461,10 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ) public onlyRole(DEFAULT_ADMIN_ROLE) { _tokenMultiplier = newMultiplier; } + + function setDefaultDelegatee( + address newDelegatee + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + defaultDelegatee = newDelegatee; + } } diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol index 2444b53..e0cb02c 100644 --- a/contracts/virtualPersona/AgentToken.sol +++ b/contracts/virtualPersona/AgentToken.sol @@ -40,7 +40,7 @@ contract AgentToken is * call autoswap when processing initial liquidity from this address. We turn this OFF when * liquidity has been loaded, and use this bool to control processing during auto-swaps * from that point onwards. */ - bool private _autoSwapInProgress = true; + bool private _autoSwapInProgress; uint128 public maxTokensPerTransaction; uint128 public maxTokensPerWallet; @@ -73,6 +73,18 @@ contract AgentToken is _; } + /** + * @dev {onlyOwnerOrFactory} + * + * Throws if called by any account other than the owner, factory or pool. + */ + modifier onlyOwnerOrFactory() { + if (owner() != _msgSender() && address(_factory) != _msgSender()) { + revert CallerIsNotAdminNorFactory(); + } + _; + } + constructor() { _disableInitializers(); } @@ -119,6 +131,7 @@ contract AgentToken is uniswapV2Pair = _createPair(); _factory = IAgentFactory(_msgSender()); + _autoSwapInProgress = true; // We don't want to tax initial liquidity } /** @@ -225,7 +238,7 @@ contract AgentToken is * * @param lpOwner The recipient of LP tokens */ - function addInitialLiquidity(address lpOwner) external onlyOwner { + function addInitialLiquidity(address lpOwner) external onlyOwnerOrFactory { _addInitialLiquidity(lpOwner); } @@ -311,13 +324,15 @@ contract AgentToken is } /** - * @dev function {addLiquidityPool} onlyOwner + * @dev function {addLiquidityPool} onlyOwnerOrFactory * * Allows the manager to add a liquidity pool to the pool enumerable set * * @param newLiquidityPool_ The address of the new liquidity pool */ - function addLiquidityPool(address newLiquidityPool_) public onlyOwner { + function addLiquidityPool( + address newLiquidityPool_ + ) public onlyOwnerOrFactory { // Don't allow calls that didn't pass an address: if (newLiquidityPool_ == address(0)) { revert LiquidityPoolCannotBeAddressZero(); @@ -332,7 +347,7 @@ contract AgentToken is } /** - * @dev function {removeLiquidityPool} onlyOwner + * @dev function {removeLiquidityPool} onlyOwnerOrFactory * * Allows the manager to remove a liquidity pool * @@ -340,7 +355,7 @@ contract AgentToken is */ function removeLiquidityPool( address removedLiquidityPool_ - ) external onlyOwner { + ) external onlyOwnerOrFactory { // Remove this from the enumerated list: _liquidityPools.remove(removedLiquidityPool_); emit LiquidityPoolRemoved(removedLiquidityPool_); @@ -374,26 +389,28 @@ contract AgentToken is } /** - * @dev function {addUnlimited} onlyOwner + * @dev function {addUnlimited} onlyOwnerOrFactory * * Allows the manager to add an unlimited address * * @param newUnlimited_ The address of the new unlimited address */ - function addUnlimited(address newUnlimited_) external onlyOwner { + function addUnlimited(address newUnlimited_) external onlyOwnerOrFactory { // Add this to the enumerated list: _unlimited.add(newUnlimited_); emit UnlimitedAddressAdded(newUnlimited_); } /** - * @dev function {removeUnlimited} onlyOwner + * @dev function {removeUnlimited} onlyOwnerOrFactory * * Allows the manager to remove an unlimited address * * @param removedUnlimited_ The address of the old removed unlimited address */ - function removeUnlimited(address removedUnlimited_) external onlyOwner { + function removeUnlimited( + address removedUnlimited_ + ) external onlyOwnerOrFactory { // Remove this from the enumerated list: _unlimited.remove(removedUnlimited_); emit UnlimitedAddressRemoved(removedUnlimited_); @@ -427,19 +444,21 @@ contract AgentToken is } /** - * @dev function {addValidCaller} onlyOwner + * @dev function {addValidCaller} onlyOwnerOrFactory * * Allows the owner to add the hash of a valid caller * * @param newValidCallerHash_ The hash of the new valid caller */ - function addValidCaller(bytes32 newValidCallerHash_) external onlyOwner { + function addValidCaller( + bytes32 newValidCallerHash_ + ) external onlyOwnerOrFactory { _validCallerCodeHashes.add(newValidCallerHash_); emit ValidCallerAdded(newValidCallerHash_); } /** - * @dev function {removeValidCaller} onlyOwner + * @dev function {removeValidCaller} onlyOwnerOrFactory * * Allows the owner to remove a valid caller * @@ -447,14 +466,14 @@ contract AgentToken is */ function removeValidCaller( bytes32 removedValidCallerHash_ - ) external onlyOwner { + ) external onlyOwnerOrFactory { // Remove this from the enumerated list: _validCallerCodeHashes.remove(removedValidCallerHash_); emit ValidCallerRemoved(removedValidCallerHash_); } /** - * @dev function {setProjectTaxRecipient} onlyOwner + * @dev function {setProjectTaxRecipient} onlyOwnerOrFactory * * Allows the manager to set the project tax recipient address * @@ -462,13 +481,13 @@ contract AgentToken is */ function setProjectTaxRecipient( address projectTaxRecipient_ - ) external onlyOwner { + ) external onlyOwnerOrFactory { projectTaxRecipient = projectTaxRecipient_; emit ProjectTaxRecipientUpdated(projectTaxRecipient_); } /** - * @dev function {setSwapThresholdBasisPoints} onlyOwner + * @dev function {setSwapThresholdBasisPoints} onlyOwnerOrFactory * * Allows the manager to set the autoswap threshold * @@ -476,7 +495,7 @@ contract AgentToken is */ function setSwapThresholdBasisPoints( uint16 swapThresholdBasisPoints_ - ) external onlyOwner { + ) external onlyOwnerOrFactory { uint256 oldswapThresholdBasisPoints = swapThresholdBasisPoints; swapThresholdBasisPoints = swapThresholdBasisPoints_; emit AutoSwapThresholdUpdated( @@ -486,7 +505,7 @@ contract AgentToken is } /** - * @dev function {setProjectTaxRates} onlyOwner + * @dev function {setProjectTaxRates} onlyOwnerOrFactory * * Change the tax rates, subject to only ever decreasing * @@ -496,7 +515,7 @@ contract AgentToken is function setProjectTaxRates( uint16 newProjectBuyTaxBasisPoints_, uint16 newProjectSellTaxBasisPoints_ - ) external onlyOwner { + ) external onlyOwnerOrFactory { uint16 oldBuyTaxBasisPoints = projectBuyTaxBasisPoints; uint16 oldSellTaxBasisPoints = projectSellTaxBasisPoints; @@ -512,7 +531,7 @@ contract AgentToken is } /** - * @dev function {setLimits} onlyOwner + * @dev function {setLimits} onlyOwnerOrFactory * * Change the limits on transactions and holdings * @@ -522,7 +541,7 @@ contract AgentToken is function setLimits( uint256 newMaxTokensPerTransaction_, uint256 newMaxTokensPerWallet_ - ) external onlyOwner { + ) external onlyOwnerOrFactory { uint256 oldMaxTokensPerTransaction = maxTokensPerTransaction; uint256 oldMaxTokensPerWallet = maxTokensPerWallet; // Limit can only be increased: @@ -984,7 +1003,7 @@ contract AgentToken is } return (amountLessTax_); } - + /** * @dev function {_autoSwap} * @@ -993,6 +1012,7 @@ contract AgentToken is * @param from_ The sender of the token * @param to_ The recipient of the token */ + function _autoSwap(address from_, address to_) internal { if (_tokenHasTax) { uint256 contractBalance = balanceOf(address(this)); @@ -1055,33 +1075,21 @@ contract AgentToken is * @param contractBalance_ The current accumulated total tax balance */ function _swapTax(uint256 swapBalance_, uint256 contractBalance_) internal { - uint256 preSwapBalance = IERC20(pairToken).balanceOf(address(this)); - address[] memory path = new address[](2); path[0] = address(this); path[1] = pairToken; // Wrap external calls in try / catch to handle errors try - _uniswapRouter.swapTokensForExactTokens( - swapBalance_, - 0, - path, - address(this), - block.timestamp + 600 - ) + _uniswapRouter + .swapExactTokensForTokensSupportingFeeOnTransferTokens( + swapBalance_, + 0, + path, + projectTaxRecipient, + block.timestamp + 600 + ) { - uint256 postSwapBalance = IERC20(pairToken).balanceOf( - address(this) - ); - - uint256 balanceToDistribute = postSwapBalance - preSwapBalance; - - uint256 totalPendingSwap = projectTaxPendingSwap; - - uint256 projectBalanceToDistribute = (balanceToDistribute * - projectTaxPendingSwap) / totalPendingSwap; - // We will not have swapped all tax tokens IF the amount was greater than the max auto swap. // We therefore cannot just set the pending swap counters to 0. Instead, in this scenario, // we must reduce them in proportion to the swap amount vs the remaining balance + swap @@ -1104,23 +1112,6 @@ contract AgentToken is } else { projectTaxPendingSwap = 0; } - // Distribute to treasuries: - bool success; - address weth; - uint256 gas; - - if (projectBalanceToDistribute > 0) { - // If no gas limit was provided or provided gas limit greater than gas left, just use the remaining gas. - gas = (CALL_GAS_LIMIT == 0 || CALL_GAS_LIMIT > gasleft()) - ? gasleft() - : CALL_GAS_LIMIT; - - // We limit the gas passed so that a called address cannot cause a block out of gas error: - IERC20(pairToken).transfer{gas: gas}( - projectTaxRecipient, - projectBalanceToDistribute - ); - } } catch { // Dont allow a failed external call (in this case to uniswap) to stop a transfer. // Emit that this has occured and continue. @@ -1162,7 +1153,7 @@ contract AgentToken is } /** - * @dev function {withdrawETH} onlyOwner + * @dev function {withdrawETH} onlyOwnerOrFactory * * A withdraw function to allow ETH to be withdrawn by the manager * @@ -1175,7 +1166,7 @@ contract AgentToken is * * @param amount_ The amount to withdraw */ - function withdrawETH(uint256 amount_) external onlyOwner { + function withdrawETH(uint256 amount_) external onlyOwnerOrFactory { (bool success, ) = _msgSender().call{value: amount_}(""); if (!success) { revert TransferFailed(); @@ -1183,7 +1174,7 @@ contract AgentToken is } /** - * @dev function {withdrawERC20} onlyOwner + * @dev function {withdrawERC20} onlyOwnerOrFactory * * A withdraw function to allow ERC20s (except address(this)) to be withdrawn. * @@ -1198,7 +1189,10 @@ contract AgentToken is * @param token_ The ERC20 contract * @param amount_ The amount to withdraw */ - function withdrawERC20(address token_, uint256 amount_) external onlyOwner { + function withdrawERC20( + address token_, + uint256 amount_ + ) external onlyOwnerOrFactory { if (token_ == address(this)) { revert CannotWithdrawThisToken(); } diff --git a/contracts/virtualPersona/IErrors.sol b/contracts/virtualPersona/IErrors.sol index 6221cb0..a8bb725 100644 --- a/contracts/virtualPersona/IErrors.sol +++ b/contracts/virtualPersona/IErrors.sol @@ -204,6 +204,8 @@ interface IErrors { error OwnerQueryForNonexistentToken(); // The token does not exist. + error CallerIsNotAdminNorFactory(); // The caller of this function must match the factory address or be an admin. + error ParametersDoNotMatchSignedMessage(); // The parameters passed with the signed message do not match the message itself. error ParamTooLargeStartDate(); // The passed parameter exceeds the var type max. diff --git a/test/agentDAO.js b/test/agentDAO.js new file mode 100644 index 0000000..d34690a --- /dev/null +++ b/test/agentDAO.js @@ -0,0 +1,393 @@ +/* +Test scenario: +1. Accounts: [validator1, staker1, validator2, staker2] +2. Stakes: [100000, 2000, 5000, 20000] +3. Uptime: [3,1] +4. All contribution NFTs are owned by account #10 +*/ +const { expect } = require("chai"); +const { toBeHex } = require("ethers/utils"); +const abi = ethers.AbiCoder.defaultAbiCoder(); +const { + loadFixture, + mine, +} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); +const { parseEther, formatEther } = require("ethers"); + +const getExecuteCallData = (factory, proposalId) => { + return factory.interface.encodeFunctionData("executeApplication", [ + proposalId, + ]); +}; + +const getMintServiceCalldata = async (serviceNft, virtualId, hash) => { + return serviceNft.interface.encodeFunctionData("mint", [virtualId, hash]); +}; + +function getDescHash(str) { + return ethers.keccak256(ethers.toUtf8Bytes(str)); +} + +describe("AgentDAO", function () { + const PROPOSAL_THRESHOLD = parseEther("100000"); //100k + const MATURITY_SCORE = toBeHex(2000, 32); // 20% + const IP_SHARE = 1000; // 10% + + const TOKEN_URI = "http://jessica"; + + const genesisInput = { + name: "Jessica", + symbol: "JSC", + tokenURI: "http://jessica", + daoName: "Jessica DAO", + cores: [0, 1, 2], + tbaSalt: + "0xa7647ac9429fdce477ebd9a95510385b756c757c26149e740abbab0ad1be2f16", + tbaImplementation: process.env.TBA_IMPLEMENTATION, + daoVotingPeriod: 600, + daoThreshold: 1000000000000000000000n, + }; + + const getAccounts = async () => { + const [ + deployer, + ipVault, + founder, + contributor1, + contributor2, + validator1, + validator2, + treasury, + trader, + ] = await ethers.getSigners(); + return { + deployer, + ipVault, + founder, + contributor1, + contributor2, + validator1, + validator2, + treasury, + trader, + }; + }; + + async function deployBaseContracts() { + const { deployer, ipVault, treasury } = await getAccounts(); + + const virtualToken = await ethers.deployContract( + "VirtualToken", + [PROPOSAL_THRESHOLD, deployer.address], + {} + ); + await virtualToken.waitForDeployment(); + + const AgentNft = await ethers.getContractFactory("AgentNft"); + const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); + + const contribution = await upgrades.deployProxy( + await ethers.getContractFactory("ContributionNft"), + [agentNft.target], + {} + ); + + const service = await upgrades.deployProxy( + await ethers.getContractFactory("ServiceNft"), + [agentNft.target, contribution.target, process.env.DATASET_SHARES], + {} + ); + + await agentNft.setContributionService(contribution.target, service.target); + + // Implementation contracts + const agentToken = await ethers.deployContract("AgentToken"); + await agentToken.waitForDeployment(); + const agentDAO = await ethers.deployContract("AgentDAO"); + await agentDAO.waitForDeployment(); + const agentVeToken = await ethers.deployContract("AgentVeToken"); + await agentVeToken.waitForDeployment(); + + const agentFactory = await upgrades.deployProxy( + await ethers.getContractFactory("AgentFactoryV2"), + [ + agentToken.target, + agentVeToken.target, + agentDAO.target, + process.env.TBA_REGISTRY, + virtualToken.target, + agentNft.target, + PROPOSAL_THRESHOLD, + deployer.address, + ] + ); + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + const minter = await ethers.deployContract("Minter", [ + service.target, + contribution.target, + agentNft.target, + IP_SHARE, + ipVault.address, + agentFactory.target, + deployer.address, + ]); + await minter.waitForDeployment(); + await agentFactory.setMinter(minter.target); + await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years + await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); + await agentFactory.setTokenAdmin(deployer.address); + await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION + ); + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address + ); + await agentFactory.setDefaultDelegatee(deployer.address); + + return { + virtualToken, + agentFactory, + agentNft, + serviceNft: service, + contributionNft: contribution, + minter, + }; + } + + async function deployWithApplication() { + const base = await deployBaseContracts(); + const { agentFactory, virtualToken } = base; + const { founder } = await getAccounts(); + + // Prepare tokens for proposal + await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); + await virtualToken + .connect(founder) + .approve(agentFactory.target, PROPOSAL_THRESHOLD); + + const tx = await agentFactory + .connect(founder) + .proposeAgent( + genesisInput.name, + genesisInput.symbol, + genesisInput.tokenURI, + genesisInput.cores, + genesisInput.tbaSalt, + genesisInput.tbaImplementation, + genesisInput.daoVotingPeriod, + genesisInput.daoThreshold + ); + + const filter = agentFactory.filters.NewApplication; + const events = await agentFactory.queryFilter(filter, -1); + const event = events[0]; + const { id } = event.args; + return { applicationId: id, ...base }; + } + + async function deployWithAgent() { + const base = await deployWithApplication(); + const { agentFactory, applicationId } = base; + + const { founder } = await getAccounts(); + await agentFactory.connect(founder).executeApplication(applicationId); + + const factoryFilter = agentFactory.filters.NewPersona; + const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); + const factoryEvent = factoryEvents[0]; + + const { virtualId, token, veToken, dao, tba, lp } = await factoryEvent.args; + + return { + ...base, + agent: { + virtualId, + token, + veToken, + dao, + tba, + lp, + }, + }; + } + + async function createContribution( + coreId, + maturity, + parentId, + isModel, + datasetId, + desc, + base, + account + ) { + const { founder } = await getAccounts(); + const { agent, serviceNft, contributionNft, minter } = base; + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + + const descHash = getDescHash(desc); + + const mintCalldata = await getMintServiceCalldata( + serviceNft, + agent.virtualId, + descHash + ); + + await agentDAO.propose([serviceNft.target], [0], [mintCalldata], desc); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); + const event = events[0]; + const proposalId = event.args[0]; + + await contributionNft.mint( + account, + agent.virtualId, + coreId, + TOKEN_URI, + proposalId, + parentId, + isModel, + datasetId + ); + + const voteParams = isModel + ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) + : "0x"; + await agentDAO + .connect(founder) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + await mine(600); + + await agentDAO.execute(proposalId); + await minter.mint(proposalId); + + return proposalId; + } + + before(async function () {}); + + it("should allow early execution when forVotes == totalSupply", async function () { + const base = await loadFixture(deployWithAgent); + const { founder, deployer } = await getAccounts(); + const { + agent, + serviceNft, + contributionNft, + minter, + virtualToken, + agentFactory, + } = base; + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + const desc = "test"; + const descHash = getDescHash(desc); + + const mintCalldata = await getMintServiceCalldata( + serviceNft, + agent.virtualId, + descHash + ); + + await agentDAO.propose([serviceNft.target], [0], [mintCalldata], desc); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); + const event = events[0]; + const proposalId = event.args[0]; + + await mine(10); + await agentDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", "0x"); + const state = await agentDAO.state(proposalId); + expect(state).to.equal(4n); + await expect(agentDAO.execute(proposalId)).to.not.rejected; + }); + + it("should not allow early execution when forVotes < totalSupply although met quorum", async function () { + const base = await loadFixture(deployWithAgent); + const { founder, deployer, trader, treasury, contributor1 } = + await getAccounts(); + const { + agent, + serviceNft, + contributionNft, + minter, + virtualToken, + agentFactory, + } = base; + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + const lpToken = await ethers.getContractAt("IUniswapV2Pair", agent.lp); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const desc = "test"; + const descHash = getDescHash(desc); + + const mintCalldata = await getMintServiceCalldata( + serviceNft, + agent.virtualId, + descHash + ); + + /////////////////////////// + // Buy from LP + /////////////////////////// + await virtualToken.mint(trader.address, parseEther("10000")); + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + await virtualToken + .connect(trader) + .approve(router.target, parseEther("20000")); + await agentToken + .connect(trader) + .approve(router.target, parseEther("20000")); + const amountToBuy = parseEther("1000"); + const capital = parseEther("10000"); + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agent.token], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + + // Provide liquidity + await router + .connect(trader) + .addLiquidity( + agentToken.target, + virtualToken.target, + parseEther("97"), + parseEther("100"), + 0, + 0, + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + const lpBalance = await lpToken.balanceOf(trader.address); + await lpToken.connect(trader).approve(veToken.target, parseEther("100")); + await veToken.connect(founder).setCanStake(true); + await veToken + .connect(trader) + .stake(lpBalance, trader.address, trader.address); + + /////////////////////////// + await agentDAO.propose([serviceNft.target], [0], [mintCalldata], desc); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); + const event = events[0]; + const proposalId = event.args[0]; + + await mine(10); + await agentDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", "0x"); + const state = await agentDAO.state(proposalId); + expect(state).to.equal(1n); + await expect(agentDAO.execute(proposalId)).to.be.rejected; + }); +}); diff --git a/test/rewardsV2.js b/test/rewardsV2.js index e96bf67..70c016c 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -57,6 +57,7 @@ describe("RewardsV2", function () { contributor2, validator1, validator2, + treasury, ] = await ethers.getSigners(); return { deployer, @@ -66,11 +67,12 @@ describe("RewardsV2", function () { contributor2, validator1, validator2, + treasury, }; }; async function deployBaseContracts() { - const { deployer, ipVault } = await getAccounts(); + const { deployer, ipVault, treasury } = await getAccounts(); const virtualToken = await ethers.deployContract( "VirtualToken", @@ -130,8 +132,20 @@ describe("RewardsV2", function () { ]); await minter.waitForDeployment(); await agentFactory.setMinter(minter.target); - await agentFactory.setMaturityDuration(86400 * 365 * 10); // 100years + await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); + await agentFactory.setTokenAdmin(deployer.address); + await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION + ); + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address + ); return { virtualToken, From 1572b08fa882c73ee269ff6d1a843ed9d0c37801 Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 6 Jun 2024 11:11:10 +0800 Subject: [PATCH 11/30] allow founder to withdraw stake amount > initial lock --- contracts/virtualPersona/AgentVeToken.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index 83f5a5b..2f17ad7 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -19,6 +19,7 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { address public agentNft; uint256 public matureAt; // The timestamp when the founder can withdraw the tokens bool public canStake; // To control private/public agent mode + uint256 private initialLock; // Initial locked amount constructor() { _disableInitializers(); @@ -62,6 +63,10 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { address sender = _msgSender(); require(amount > 0, "Cannot stake 0"); + if (totalSupply() == 0) { + initialLock = amount; + } + IAgentNft registry = IAgentNft(agentNft); uint256 virtualId = registry.stakingTokenToVirtualId(address(this)); registry.addValidator(virtualId, delegatee); @@ -92,7 +97,8 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { function withdraw(uint256 amount) public noReentrant { address sender = _msgSender(); require(balanceOf(sender) >= amount, "Insufficient balance"); - if (sender == founder) { + + if ((sender == founder) && ((balanceOf(sender) - amount) < initialLock)) { require(block.timestamp >= matureAt, "Not mature yet"); } From 443d951b2086ea7d437fec37bd1b927b363e46ee Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 6 Jun 2024 11:52:21 +0800 Subject: [PATCH 12/30] prepare for migration --- contracts/{AgentRewardV2.sol.old => AgentRewardV2.sol} | 0 scripts/{ => v1}/deployContributions.ts | 2 +- scripts/{ => v1}/deployGovernance.ts | 0 scripts/{ => v1}/deployPersonaFactory.ts | 2 +- scripts/{ => v1}/deployReward.ts | 0 scripts/{ => v1}/deployRewardTreasury.ts | 2 +- scripts/{ => v1}/deployRewardV2.ts | 0 scripts/{ => v1}/deployService.ts | 2 +- scripts/{ => v1}/deployTimeLock.ts | 2 +- scripts/{ => v1}/deployVetoken.ts | 0 scripts/{ => v1}/deployVirtualGenesis.ts | 2 +- scripts/{ => v1}/deployVirtualImplementations.ts | 0 scripts/{ => v1}/deployVirtualNft.ts | 2 +- scripts/{ => v1}/upgradeFactory.ts | 0 scripts/{upgradeRewards.ts => v2/migrateV2.ts} | 5 +++++ 15 files changed, 12 insertions(+), 7 deletions(-) rename contracts/{AgentRewardV2.sol.old => AgentRewardV2.sol} (100%) rename scripts/{ => v1}/deployContributions.ts (90%) rename scripts/{ => v1}/deployGovernance.ts (100%) rename scripts/{ => v1}/deployPersonaFactory.ts (90%) rename scripts/{ => v1}/deployReward.ts (100%) rename scripts/{ => v1}/deployRewardTreasury.ts (81%) rename scripts/{ => v1}/deployRewardV2.ts (100%) rename scripts/{ => v1}/deployService.ts (93%) rename scripts/{ => v1}/deployTimeLock.ts (90%) rename scripts/{ => v1}/deployVetoken.ts (100%) rename scripts/{ => v1}/deployVirtualGenesis.ts (78%) rename scripts/{ => v1}/deployVirtualImplementations.ts (100%) rename scripts/{ => v1}/deployVirtualNft.ts (87%) rename scripts/{ => v1}/upgradeFactory.ts (100%) rename scripts/{upgradeRewards.ts => v2/migrateV2.ts} (82%) diff --git a/contracts/AgentRewardV2.sol.old b/contracts/AgentRewardV2.sol similarity index 100% rename from contracts/AgentRewardV2.sol.old rename to contracts/AgentRewardV2.sol diff --git a/scripts/deployContributions.ts b/scripts/v1/deployContributions.ts similarity index 90% rename from scripts/deployContributions.ts rename to scripts/v1/deployContributions.ts index fddc687..2098c07 100644 --- a/scripts/deployContributions.ts +++ b/scripts/v1/deployContributions.ts @@ -2,7 +2,7 @@ import { ethers, upgrades } from "hardhat"; (async () => { try { - const args = require("./arguments/contributionNft"); + const args = require("../arguments/contributionNft"); const Contribution = await ethers.getContractFactory("ContributionNft"); const contribution = await upgrades.deployProxy( Contribution, diff --git a/scripts/deployGovernance.ts b/scripts/v1/deployGovernance.ts similarity index 100% rename from scripts/deployGovernance.ts rename to scripts/v1/deployGovernance.ts diff --git a/scripts/deployPersonaFactory.ts b/scripts/v1/deployPersonaFactory.ts similarity index 90% rename from scripts/deployPersonaFactory.ts rename to scripts/v1/deployPersonaFactory.ts index 79e5ea0..fdf32ca 100644 --- a/scripts/deployPersonaFactory.ts +++ b/scripts/v1/deployPersonaFactory.ts @@ -1,5 +1,5 @@ import { ethers, upgrades } from "hardhat"; -const deployParams = require("./arguments/personaFactoryArguments.js"); +const deployParams = require("../arguments/personaFactoryArguments.js"); (async () => { try { diff --git a/scripts/deployReward.ts b/scripts/v1/deployReward.ts similarity index 100% rename from scripts/deployReward.ts rename to scripts/v1/deployReward.ts diff --git a/scripts/deployRewardTreasury.ts b/scripts/v1/deployRewardTreasury.ts similarity index 81% rename from scripts/deployRewardTreasury.ts rename to scripts/v1/deployRewardTreasury.ts index 6374a05..d93d429 100644 --- a/scripts/deployRewardTreasury.ts +++ b/scripts/v1/deployRewardTreasury.ts @@ -1,5 +1,5 @@ import { ethers } from "hardhat"; -const deployArguments = require("./arguments/rewardTreasuryArguments"); +const deployArguments = require("../arguments/rewardTreasuryArguments"); (async () => { try { diff --git a/scripts/deployRewardV2.ts b/scripts/v1/deployRewardV2.ts similarity index 100% rename from scripts/deployRewardV2.ts rename to scripts/v1/deployRewardV2.ts diff --git a/scripts/deployService.ts b/scripts/v1/deployService.ts similarity index 93% rename from scripts/deployService.ts rename to scripts/v1/deployService.ts index fa2ab23..db9d4dd 100644 --- a/scripts/deployService.ts +++ b/scripts/v1/deployService.ts @@ -2,7 +2,7 @@ import { ethers, upgrades } from "hardhat"; (async () => { try { - const args = require("./arguments/serviceNft"); + const args = require("../arguments/serviceNft"); const Service = await ethers.getContractFactory("ServiceNft"); const service = await upgrades.deployProxy(Service, args, { diff --git a/scripts/deployTimeLock.ts b/scripts/v1/deployTimeLock.ts similarity index 90% rename from scripts/deployTimeLock.ts rename to scripts/v1/deployTimeLock.ts index 006dfe0..c48854d 100644 --- a/scripts/deployTimeLock.ts +++ b/scripts/v1/deployTimeLock.ts @@ -1,5 +1,5 @@ import { ethers } from "hardhat"; -const deployArguments = require("./arguments/lockArguments"); +const deployArguments = require("../arguments/lockArguments"); (async () => { try { diff --git a/scripts/deployVetoken.ts b/scripts/v1/deployVetoken.ts similarity index 100% rename from scripts/deployVetoken.ts rename to scripts/v1/deployVetoken.ts diff --git a/scripts/deployVirtualGenesis.ts b/scripts/v1/deployVirtualGenesis.ts similarity index 78% rename from scripts/deployVirtualGenesis.ts rename to scripts/v1/deployVirtualGenesis.ts index 78da22b..e43979a 100644 --- a/scripts/deployVirtualGenesis.ts +++ b/scripts/v1/deployVirtualGenesis.ts @@ -1,6 +1,6 @@ import { ethers } from "hardhat"; -const deployParams = require("./arguments/genesisDaoArguments.js"); +const deployParams = require("../arguments/genesisDaoArguments.js"); (async () => { try { diff --git a/scripts/deployVirtualImplementations.ts b/scripts/v1/deployVirtualImplementations.ts similarity index 100% rename from scripts/deployVirtualImplementations.ts rename to scripts/v1/deployVirtualImplementations.ts diff --git a/scripts/deployVirtualNft.ts b/scripts/v1/deployVirtualNft.ts similarity index 87% rename from scripts/deployVirtualNft.ts rename to scripts/v1/deployVirtualNft.ts index 19ffcac..fc2b38d 100644 --- a/scripts/deployVirtualNft.ts +++ b/scripts/v1/deployVirtualNft.ts @@ -1,6 +1,6 @@ import { ethers, upgrades } from "hardhat"; -const deployParams = require("./arguments/nft.js"); +const deployParams = require("../arguments/nft.js"); (async () => { try { diff --git a/scripts/upgradeFactory.ts b/scripts/v1/upgradeFactory.ts similarity index 100% rename from scripts/upgradeFactory.ts rename to scripts/v1/upgradeFactory.ts diff --git a/scripts/upgradeRewards.ts b/scripts/v2/migrateV2.ts similarity index 82% rename from scripts/upgradeRewards.ts rename to scripts/v2/migrateV2.ts index 46eb3f8..a121f6e 100644 --- a/scripts/upgradeRewards.ts +++ b/scripts/v2/migrateV2.ts @@ -1,7 +1,12 @@ import { ethers, upgrades } from "hardhat"; +async function migrateImplementations(){ + +} + (async () => { try { + // Deploy impl const Contract = await ethers.getContractFactory("AgentRewardV2"); const contract = await upgrades.upgradeProxy(process.env.PERSONA_REWARD, Contract); console.log("Upgraded", contract.target) From 73236ee936da0ebb8dd3f21719b396ff154821e2 Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 6 Jun 2024 16:01:45 +0800 Subject: [PATCH 13/30] added migration script --- .openzeppelin/base-sepolia.json | 1332 ++++++++++++++++- AgentNft-2-storage-layout.json | 265 ++++ AgentNft-storage-layout.json | 265 ++++ contracts/AgentRewardV2.sol | 645 +------- contracts/virtualPersona/AgentFactory.sol | 31 +- .../{AgentNft.sol => AgentNftV2.sol} | 24 +- contracts/virtualPersona/IAgentNft.sol | 7 +- hardhat.config.js | 1 + scripts/v2/migrateV2.ts | 75 +- 9 files changed, 1981 insertions(+), 664 deletions(-) create mode 100644 AgentNft-2-storage-layout.json create mode 100644 AgentNft-storage-layout.json rename contracts/virtualPersona/{AgentNft.sol => AgentNftV2.sol} (94%) diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json index d692914..e5fa923 100644 --- a/.openzeppelin/base-sepolia.json +++ b/.openzeppelin/base-sepolia.json @@ -2204,7 +2204,7 @@ "label": "virtualInfos", "offset": 0, "slot": "8", - "type": "t_mapping(t_uint256,t_struct(VirtualInfo)11161_storage)", + "type": "t_mapping(t_uint256,t_struct(VirtualInfo)11167_storage)", "contract": "AgentNft", "src": "contracts/virtualPersona/AgentNft.sol:47" }, @@ -2408,7 +2408,7 @@ "label": "mapping(uint256 => mapping(address => bool))", "numberOfBytes": "32" }, - "t_mapping(t_uint256,t_struct(VirtualInfo)11161_storage)": { + "t_mapping(t_uint256,t_struct(VirtualInfo)11167_storage)": { "label": "mapping(uint256 => struct IAgentNft.VirtualInfo)", "numberOfBytes": "32" }, @@ -2420,7 +2420,7 @@ "label": "mapping(uint8 => string)", "numberOfBytes": "32" }, - "t_struct(VirtualInfo)11161_storage": { + "t_struct(VirtualInfo)11167_storage": { "label": "struct IAgentNft.VirtualInfo", "members": [ { @@ -6638,7 +6638,7 @@ "label": "_roles", "offset": 0, "slot": "0", - "type": "t_mapping(t_bytes32,t_struct(RoleData)275_storage)", + "type": "t_mapping(t_bytes32,t_struct(RoleData)7540_storage)", "contract": "AccessControl", "src": "@openzeppelin/contracts/access/AccessControl.sol:55" }, @@ -6726,7 +6726,7 @@ "label": "_applications", "offset": 0, "slot": "11", - "type": "t_mapping(t_uint256,t_struct(Application)2278_storage)", + "type": "t_mapping(t_uint256,t_struct(Application)27723_storage)", "contract": "AgentFactory", "src": "contracts/virtualPersona/AgentFactory.sol:65" }, @@ -6760,7 +6760,7 @@ "label": "bool", "numberOfBytes": "1" }, - "t_struct(InitializableStorage)10_storage": { + "t_struct(InitializableStorage)744_storage": { "label": "struct Initializable.InitializableStorage", "members": [ { @@ -6798,7 +6798,7 @@ "label": "bytes32", "numberOfBytes": "32" }, - "t_enum(ApplicationStatus)2249": { + "t_enum(ApplicationStatus)27694": { "label": "enum AgentFactory.ApplicationStatus", "members": [ "Active", @@ -6811,11 +6811,11 @@ "label": "mapping(address => bool)", "numberOfBytes": "32" }, - "t_mapping(t_bytes32,t_struct(RoleData)275_storage)": { + "t_mapping(t_bytes32,t_struct(RoleData)7540_storage)": { "label": "mapping(bytes32 => struct AccessControl.RoleData)", "numberOfBytes": "32" }, - "t_mapping(t_uint256,t_struct(Application)2278_storage)": { + "t_mapping(t_uint256,t_struct(Application)27723_storage)": { "label": "mapping(uint256 => struct AgentFactory.Application)", "numberOfBytes": "32" }, @@ -6823,7 +6823,7 @@ "label": "string", "numberOfBytes": "32" }, - "t_struct(Application)2278_storage": { + "t_struct(Application)27723_storage": { "label": "struct AgentFactory.Application", "members": [ { @@ -6846,7 +6846,7 @@ }, { "label": "status", - "type": "t_enum(ApplicationStatus)2249", + "type": "t_enum(ApplicationStatus)27694", "offset": 0, "slot": "3" }, @@ -6907,7 +6907,7 @@ ], "numberOfBytes": "384" }, - "t_struct(RoleData)275_storage": { + "t_struct(RoleData)7540_storage": { "label": "struct AccessControl.RoleData", "members": [ { @@ -7442,6 +7442,1314 @@ ] } } + }, + "bc0bde00f6217681d66f102ff78d333594fca8c7b667f9b4b7f58e1aff5dcfe4": { + "address": "0xc9eB596fb3d1F43f1383a235CD1dF2762674Ff2c", + "txHash": "0x927a2c2f85e049918b95dff25877ad3ef7cff75eba6871599f545d4005408520", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)3895_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:23" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:24" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:25" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:26" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:33" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)15833_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:70" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:72" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:83" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:85" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:97" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:98" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:99" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:100" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:107" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)211_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)15804": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)3895_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)15833_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)15833_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)15804", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)3895_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "a09abecb208e41d57e44cd6fb506b4f25be94af62e1a67def67a027a49ad2ccf": { + "address": "0xF25A13d4D71375bE728B3eE337605047ECeE487C", + "txHash": "0x5db52641c41c244790b4733da618effbeb30f0a600e528532b4e7ec88d10a0cc", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)275_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:23" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:24" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:25" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:26" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:33" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)2321_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:70" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:72" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:83" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:85" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:97" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:98" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:99" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:100" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:107" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)10_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)2292": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)275_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)2321_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)2321_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)2292", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)275_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "e10a30c86b83904166af84da2d83204bf5ffc9265d549b765929e1be23a49090": { + "address": "0x426A2a2fcdA4851E411a5afDa7480263bd02E254", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "coreTypes", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_uint8,t_string_storage)", + "contract": "CoreRegistry", + "src": "contracts/virtualPersona/CoreRegistry.sol:7" + }, + { + "label": "_nextCoreType", + "offset": 0, + "slot": "1", + "type": "t_uint8", + "contract": "CoreRegistry", + "src": "contracts/virtualPersona/CoreRegistry.sol:8" + }, + { + "label": "_validatorsMap", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:8" + }, + { + "label": "_baseValidatorScore", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:10" + }, + { + "label": "_validators", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_array(t_address)dyn_storage)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:12" + }, + { + "label": "_getScoreOf", + "offset": 0, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:14" + }, + { + "label": "_getMaxScore", + "offset": 8, + "slot": "5", + "type": "t_function_internal_view(t_uint256)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:15" + }, + { + "label": "_getPastScore", + "offset": 16, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:16" + }, + { + "label": "_nextVirtualId", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:27" + }, + { + "label": "_stakingTokenToVirtualId", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:28" + }, + { + "label": "virtualInfos", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_struct(VirtualInfo)11252_storage)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:47" + }, + { + "label": "_contributionNft", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:49" + }, + { + "label": "_serviceNft", + "offset": 0, + "slot": "10", + "type": "t_address", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:50" + }, + { + "label": "_blacklists", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:54" + }, + { + "label": "virtualLPs", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_uint256,t_struct(VirtualLP)11264_storage)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:55" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)25_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_string_storage)": { + "label": "mapping(uint256 => string)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)25_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ERC721Storage)191_storage": { + "label": "struct ERC721Upgradeable.ERC721Storage", + "members": [ + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "2" + }, + { + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "offset": 0, + "slot": "3" + }, + { + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "4" + }, + { + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_struct(ERC721URIStorageStorage)329_storage": { + "label": "struct ERC721URIStorageUpgradeable.ERC721URIStorageStorage", + "members": [ + { + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)93_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)25_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_function_internal_view(t_uint256)returns(t_uint256)": { + "label": "function (uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address)returns(t_uint256)": { + "label": "function (uint256,address) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)": { + "label": "function (uint256,address,uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_address)dyn_storage)": { + "label": "mapping(uint256 => address[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { + "label": "mapping(uint256 => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(VirtualInfo)11252_storage)": { + "label": "mapping(uint256 => struct IAgentNft.VirtualInfo)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(VirtualLP)11264_storage)": { + "label": "mapping(uint256 => struct IAgentNft.VirtualLP)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint8,t_string_storage)": { + "label": "mapping(uint8 => string)", + "numberOfBytes": "32" + }, + "t_struct(VirtualInfo)11252_storage": { + "label": "struct IAgentNft.VirtualInfo", + "members": [ + { + "label": "dao", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "founder", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "tba", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "coreTypes", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(VirtualLP)11264_storage": { + "label": "struct IAgentNft.VirtualLP", + "members": [ + { + "label": "pool", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "veToken", + "type": "t_address", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)25_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721URIStorage": [ + { + "contract": "ERC721URIStorageUpgradeable", + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol:25", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721": [ + { + "contract": "ERC721Upgradeable", + "label": "_name", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:27", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC721Upgradeable", + "label": "_symbol", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:30", + "offset": 0, + "slot": "1" + }, + { + "contract": "ERC721Upgradeable", + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:32", + "offset": 0, + "slot": "2" + }, + { + "contract": "ERC721Upgradeable", + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:34", + "offset": 0, + "slot": "3" + }, + { + "contract": "ERC721Upgradeable", + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:36", + "offset": 0, + "slot": "4" + }, + { + "contract": "ERC721Upgradeable", + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:38", + "offset": 0, + "slot": "5" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + }, + "allAddresses": [ + "0x426A2a2fcdA4851E411a5afDa7480263bd02E254", + "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3" + ] } } } diff --git a/AgentNft-2-storage-layout.json b/AgentNft-2-storage-layout.json new file mode 100644 index 0000000..a56dc34 --- /dev/null +++ b/AgentNft-2-storage-layout.json @@ -0,0 +1,265 @@ +{ + "storage": [ + { + "astId": 20008, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "coreTypes", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_uint8,t_string_storage)" + }, + { + "astId": 20010, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_nextCoreType", + "offset": 0, + "slot": "1", + "type": "t_uint8" + }, + { + "astId": 21242, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_validatorsMap", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))" + }, + { + "astId": 21248, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_baseValidatorScore", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))" + }, + { + "astId": 21253, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_validators", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_array(t_address)dyn_storage)" + }, + { + "astId": 21263, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_getScoreOf", + "offset": 0, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address)returns(t_uint256)" + }, + { + "astId": 21271, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_getMaxScore", + "offset": 8, + "slot": "5", + "type": "t_function_internal_view(t_uint256)returns(t_uint256)" + }, + { + "astId": 21283, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_getPastScore", + "offset": 16, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)" + }, + { + "astId": 18899, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_nextVirtualId", + "offset": 0, + "slot": "6", + "type": "t_uint256" + }, + { + "astId": 18903, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_stakingTokenToVirtualId", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 18947, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "virtualInfos", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_struct(VirtualInfo)20267_storage)" + }, + { + "astId": 18949, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_contributionNft", + "offset": 0, + "slot": "9", + "type": "t_address" + }, + { + "astId": 18951, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_serviceNft", + "offset": 0, + "slot": "10", + "type": "t_address" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "base": "t_address", + "encoding": "dynamic_array", + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "base": "t_uint8", + "encoding": "dynamic_array", + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_function_internal_view(t_uint256)returns(t_uint256)": { + "encoding": "inplace", + "label": "function (uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address)returns(t_uint256)": { + "encoding": "inplace", + "label": "function (uint256,address) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)": { + "encoding": "inplace", + "label": "function (uint256,address,uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_uint256,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint256,t_array(t_address)dyn_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => address[])", + "numberOfBytes": "32", + "value": "t_array(t_address)dyn_storage" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => mapping(address => bool))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_bool)" + }, + "t_mapping(t_uint256,t_struct(VirtualInfo)20267_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct IAgentNft.VirtualInfo)", + "numberOfBytes": "32", + "value": "t_struct(VirtualInfo)20267_storage" + }, + "t_mapping(t_uint256,t_uint256)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint8,t_string_storage)": { + "encoding": "mapping", + "key": "t_uint8", + "label": "mapping(uint8 => string)", + "numberOfBytes": "32", + "value": "t_string_storage" + }, + "t_string_storage": { + "encoding": "bytes", + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(VirtualInfo)20267_storage": { + "encoding": "inplace", + "label": "struct IAgentNft.VirtualInfo", + "members": [ + { + "astId": 20257, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "dao", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 20259, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "token", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 20261, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "founder", + "offset": 0, + "slot": "2", + "type": "t_address" + }, + { + "astId": 20263, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "tba", + "offset": 0, + "slot": "3", + "type": "t_address" + }, + { + "astId": 20266, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "coreTypes", + "offset": 0, + "slot": "4", + "type": "t_array(t_uint8)dyn_storage" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "encoding": "inplace", + "label": "uint8", + "numberOfBytes": "1" + } + } +} \ No newline at end of file diff --git a/AgentNft-storage-layout.json b/AgentNft-storage-layout.json new file mode 100644 index 0000000..959d157 --- /dev/null +++ b/AgentNft-storage-layout.json @@ -0,0 +1,265 @@ +{ + "storage": [ + { + "astId": 11021, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "coreTypes", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_uint8,t_string_storage)" + }, + { + "astId": 11023, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_nextCoreType", + "offset": 0, + "slot": "1", + "type": "t_uint8" + }, + { + "astId": 11335, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_validatorsMap", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))" + }, + { + "astId": 11341, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_baseValidatorScore", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))" + }, + { + "astId": 11346, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_validators", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_array(t_address)dyn_storage)" + }, + { + "astId": 11356, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_getScoreOf", + "offset": 0, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address)returns(t_uint256)" + }, + { + "astId": 11364, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_getMaxScore", + "offset": 8, + "slot": "5", + "type": "t_function_internal_view(t_uint256)returns(t_uint256)" + }, + { + "astId": 11376, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_getPastScore", + "offset": 16, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)" + }, + { + "astId": 10364, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_nextVirtualId", + "offset": 0, + "slot": "6", + "type": "t_uint256" + }, + { + "astId": 10368, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_stakingTokenToVirtualId", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 10412, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "virtualInfos", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_struct(VirtualInfo)11161_storage)" + }, + { + "astId": 10414, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_contributionNft", + "offset": 0, + "slot": "9", + "type": "t_address" + }, + { + "astId": 10416, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "_serviceNft", + "offset": 0, + "slot": "10", + "type": "t_address" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "base": "t_address", + "encoding": "dynamic_array", + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "base": "t_uint8", + "encoding": "dynamic_array", + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_function_internal_view(t_uint256)returns(t_uint256)": { + "encoding": "inplace", + "label": "function (uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address)returns(t_uint256)": { + "encoding": "inplace", + "label": "function (uint256,address) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)": { + "encoding": "inplace", + "label": "function (uint256,address,uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_uint256,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint256,t_array(t_address)dyn_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => address[])", + "numberOfBytes": "32", + "value": "t_array(t_address)dyn_storage" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => mapping(address => bool))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_bool)" + }, + "t_mapping(t_uint256,t_struct(VirtualInfo)11161_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct IAgentNft.VirtualInfo)", + "numberOfBytes": "32", + "value": "t_struct(VirtualInfo)11161_storage" + }, + "t_mapping(t_uint256,t_uint256)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint8,t_string_storage)": { + "encoding": "mapping", + "key": "t_uint8", + "label": "mapping(uint8 => string)", + "numberOfBytes": "32", + "value": "t_string_storage" + }, + "t_string_storage": { + "encoding": "bytes", + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(VirtualInfo)11161_storage": { + "encoding": "inplace", + "label": "struct IAgentNft.VirtualInfo", + "members": [ + { + "astId": 11151, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "dao", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 11153, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "token", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 11155, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "founder", + "offset": 0, + "slot": "2", + "type": "t_address" + }, + { + "astId": 11157, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "tba", + "offset": 0, + "slot": "3", + "type": "t_address" + }, + { + "astId": 11160, + "contract": "contracts/virtualPersona/AgentNft.sol:AgentNft", + "label": "coreTypes", + "offset": 0, + "slot": "4", + "type": "t_array(t_uint8)dyn_storage" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "encoding": "inplace", + "label": "uint8", + "numberOfBytes": "1" + } + } +} \ No newline at end of file diff --git a/contracts/AgentRewardV2.sol b/contracts/AgentRewardV2.sol index 97ce644..4e0ab82 100644 --- a/contracts/AgentRewardV2.sol +++ b/contracts/AgentRewardV2.sol @@ -16,12 +16,7 @@ import "./contribution/IServiceNft.sol"; import "./libs/TokenSaver.sol"; import "./IAgentReward.sol"; -contract AgentRewardV2 is - IAgentReward, - Initializable, - AccessControl, - TokenSaver -{ +contract AgentRewardV2 { using Math for uint256; using SafeERC20 for IERC20; using RewardSettingsCheckpoints for RewardSettingsCheckpoints.Trace; @@ -37,31 +32,12 @@ contract AgentRewardV2 is address public contributionNft; address public serviceNft; - // Rewards checkpoints, split into Master reward and Virtual shares - MainReward[] private _mainRewards; - mapping(uint256 virtualId => Reward[]) private _rewards; - - RewardSettingsCheckpoints.Trace private _rewardSettings; - - // Rewards ledger - uint256 public protocolRewards; - uint256 public validatorPoolRewards; // Accumulate the penalties from missed proposal voting - mapping(address account => mapping(uint256 virtualId => Claim claim)) - private _claimedStakerRewards; - mapping(address account => mapping(uint256 virtualId => Claim claim)) - private _claimedValidatorRewards; - mapping(uint256 serviceId => ServiceReward) private _serviceRewards; - mapping(uint48 rewardId => mapping(uint8 coreType => uint256 impacts)) _rewardImpacts; - - mapping(address validator => mapping(uint48 rewardId => uint256 amount)) - private _validatorRewards; - - modifier onlyGov() { - if (!hasRole(GOV_ROLE, _msgSender())) { - revert NotGovError(); - } - _; - } + // modifier onlyGov() { + // if (!hasRole(GOV_ROLE, _msgSender())) { + // revert NotGovError(); + // } + // _; + // } bool internal locked; @@ -72,596 +48,19 @@ contract AgentRewardV2 is locked = false; } - function initialize( - address rewardToken_, - address agentNft_, - address contributionNft_, - address serviceNft_, - RewardSettingsCheckpoints.RewardSettings memory settings_ - ) external initializer { - rewardToken = rewardToken_; - agentNft = agentNft_; - contributionNft = contributionNft_; - serviceNft = serviceNft_; - _rewardSettings.push(0, settings_); - _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); - _nextRewardId = 1; - } - - function getRewardSettings() - public - view - returns (RewardSettingsCheckpoints.RewardSettings memory) - { - return _rewardSettings.latest(); - } - - function getPastRewardSettings( - uint32 timepoint - ) public view returns (RewardSettingsCheckpoints.RewardSettings memory) { - uint32 currentTimepoint = SafeCast.toUint32(block.number); - if (timepoint >= currentTimepoint) { - revert ERC5805FutureLookup(timepoint, currentTimepoint); - } - return _rewardSettings.upperLookupRecent(timepoint); - } - - function getMainReward(uint32 pos) public view returns (MainReward memory) { - return _mainRewards[pos]; - } - - function getReward( - uint256 virtualId, - uint32 pos - ) public view returns (Reward memory) { - return _rewards[virtualId][pos]; - } - - function rewardCount(uint256 virtualId) public view returns (uint256) { - return _rewards[virtualId].length; - } - - // ---------------- - // Distribute rewards - // ---------------- - - function distributeRewards(uint256 amount) public onlyGov returns (uint32) { - require(amount > 0, "Invalid amount"); - - IERC20(rewardToken).safeTransferFrom( - _msgSender(), - address(this), - amount - ); - - RewardSettingsCheckpoints.RewardSettings - memory settings = getRewardSettings(); - - uint256 protocolShares = _distributeProtocolRewards(amount); - - uint256 agentShares = amount - protocolShares; - _prepareAgentsRewards(agentShares, settings); - return SafeCast.toUint32(_mainRewards.length - 1); - } - - function distributeRewardsForAgents( - uint32 mainRewardIndex, - uint256[] memory virtualIds - ) public onlyGov { - RewardSettingsCheckpoints.RewardSettings - memory settings = getRewardSettings(); - - for (uint256 i = 0; i < virtualIds.length; i++) { - _distributeAgentRewards(virtualIds[i], mainRewardIndex, settings); - } - } - - function _distributeProtocolRewards( - uint256 amount - ) private returns (uint256) { - RewardSettingsCheckpoints.RewardSettings - memory rewardSettings = _rewardSettings.latest(); - uint256 protocolShares = (amount * rewardSettings.protocolShares) / - DENOMINATOR; - protocolRewards += protocolShares; - return protocolShares; - } - - // Prepare agent reward placeholders and calculate total staked tokens for all eligible agents - function _prepareAgentsRewards( - uint256 amount, - RewardSettingsCheckpoints.RewardSettings memory settings - ) private returns (uint256 agentCount) { - IAgentNft nft = IAgentNft(agentNft); - uint256 grandTotalStaked = 0; // Total staked amount for all personas - uint256 totalAgents = nft.totalSupply(); - uint32 mainPos = SafeCast.toUint32(_mainRewards.length); - - // Get staking amount for all agents - for (uint256 virtualId = 1; virtualId <= totalAgents; virtualId++) { - // Get staked amount - uint256 totalStaked = nft.totalStaked(virtualId); - if (totalStaked < settings.stakeThreshold) { - continue; - } - - agentCount++; - grandTotalStaked += totalStaked; - uint48 rewardId = _nextRewardId++; - - _rewards[virtualId].push( - Reward({ - id: rewardId, - mainIndex: mainPos, - totalStaked: totalStaked, - validatorAmount: 0, - contributorAmount: 0, - coreAmount: 0 - }) - ); - } - - _mainRewards.push( - MainReward( - SafeCast.toUint32(block.number), - amount, - agentCount, - grandTotalStaked - ) - ); - emit NewMainReward(mainPos, amount, agentCount, grandTotalStaked); - } - - // Calculate agent rewards based on staked weightage and distribute to all stakers, validators and contributors - function _distributeAgentRewards( - uint256 virtualId, - uint32 mainRewardIndex, - RewardSettingsCheckpoints.RewardSettings memory settings - ) private { - if (_rewards[virtualId].length == 0) { - return; - } - - MainReward memory mainReward = _mainRewards[mainRewardIndex]; - - Reward storage reward = _rewards[virtualId][ - _rewards[virtualId].length - 1 - ]; - if (reward.mainIndex != mainRewardIndex) { - return; - } - // Prevent double entry - if (reward.validatorAmount > 0 || reward.contributorAmount > 0) { - return; - } - - // Calculate VIRTUAL reward based on staked weightage - uint256 amount = (mainReward.amount * reward.totalStaked) / - mainReward.totalStaked; - - reward.contributorAmount = - (amount * uint256(settings.contributorShares)) / - DENOMINATOR; - reward.validatorAmount = amount - reward.contributorAmount; - - _distributeValidatorRewards( - reward.validatorAmount, - virtualId, - reward.id, - reward.totalStaked - ); - _distributeContributorRewards( - reward.contributorAmount, - virtualId, - settings - ); - - emit NewAgentReward( - mainRewardIndex, - virtualId, - reward.validatorAmount, - reward.contributorAmount, - reward.coreAmount - ); - } - - // Calculate validator rewards based on votes weightage and participation rate - function _distributeValidatorRewards( - uint256 amount, - uint256 virtualId, - uint48 rewardId, - uint256 totalStaked - ) private { - IAgentNft nft = IAgentNft(agentNft); - // Calculate weighted validator shares - uint256 validatorCount = nft.validatorCount(virtualId); - uint256 totalProposals = nft.totalProposals(virtualId); - - for (uint256 i = 0; i < validatorCount; i++) { - address validator = nft.validatorAt(virtualId, i); - - // Get validator revenue by votes weightage - address stakingAddress = nft.virtualInfo(virtualId).token; - uint256 votes = IERC5805(stakingAddress).getVotes(validator); - uint256 validatorRewards = (amount * votes) / totalStaked; - - // Calc validator reward based on participation rate - uint256 participationReward = totalProposals == 0 - ? 0 - : (validatorRewards * - nft.validatorScore(virtualId, validator)) / totalProposals; - _validatorRewards[validator][rewardId] = participationReward; - - validatorPoolRewards += validatorRewards - participationReward; - } - } - - function _distributeContributorRewards( - uint256 amount, - uint256 virtualId, - RewardSettingsCheckpoints.RewardSettings memory settings - ) private { - IAgentNft nft = IAgentNft(agentNft); - uint8[] memory coreTypes = nft.virtualInfo(virtualId).coreTypes; - IServiceNft serviceNftContract = IServiceNft(serviceNft); - IContributionNft contributionNftContract = IContributionNft( - contributionNft - ); - - Reward storage reward = _rewards[virtualId][ - _rewards[virtualId].length - 1 - ]; - reward.coreAmount = amount / coreTypes.length; - uint256[] memory services = nft.getAllServices(virtualId); - - // Populate service impacts - uint256 serviceId; - uint256 impact; - for (uint i = 0; i < services.length; i++) { - serviceId = services[i]; - impact = serviceNftContract.getImpact(serviceId); - if (impact == 0) { - continue; - } - - ServiceReward storage serviceReward = _serviceRewards[serviceId]; - if (serviceReward.impact == 0) { - serviceReward.impact = impact; - } - _rewardImpacts[reward.id][ - serviceNftContract.getCore(serviceId) - ] += impact; - } - - // Distribute service rewards - uint256 impactAmount = 0; - uint256 parentAmount = 0; - uint256 parentShares = uint256(settings.parentShares); - for (uint i = 0; i < services.length; i++) { - serviceId = services[i]; - ServiceReward storage serviceReward = _serviceRewards[serviceId]; - if (serviceReward.impact == 0) { - continue; - } - impactAmount = - (reward.coreAmount * serviceReward.impact) / - _rewardImpacts[reward.id][ - serviceNftContract.getCore(serviceId) - ]; - parentAmount = contributionNftContract.getParentId(serviceId) == 0 - ? 0 - : ((impactAmount * parentShares) / DENOMINATOR); - - serviceReward.amount += impactAmount - parentAmount; - serviceReward.parentAmount += parentAmount; - } - } - - // ---------------- - // Functions to query rewards - // ---------------- - function _getClaimableStakerRewardsAt( - uint256 pos, - uint256 virtualId, - address account, - address stakingAddress - ) private view returns (uint256) { - Reward memory reward = getReward(virtualId, SafeCast.toUint32(pos)); - MainReward memory mainReward = getMainReward(reward.mainIndex); - IAgentToken token = IAgentToken(stakingAddress); - - address delegatee = token.getPastDelegates( - account, - mainReward.blockNumber - ); - - if (delegatee == address(0)) { - return 0; - } - - RewardSettingsCheckpoints.RewardSettings - memory settings = getPastRewardSettings(mainReward.blockNumber); - - uint256 validatorGroupRewards = _validatorRewards[delegatee][reward.id]; - - uint256 tokens = token.getPastBalanceOf( - account, - mainReward.blockNumber - ); - uint256 votes = IERC5805(stakingAddress).getPastVotes( - delegatee, - mainReward.blockNumber - ); - - return - (((validatorGroupRewards * tokens) / votes) * - uint256(settings.stakerShares)) / DENOMINATOR; - } - - function _getClaimableStakerRewards( - address staker, - uint256 virtualId - ) internal view returns (uint256) { - uint256 count = rewardCount(virtualId); - if (count == 0) { - return 0; - } - - address stakingAddress = IAgentNft(agentNft) - .virtualInfo(virtualId) - .token; - - Claim memory claim = _claimedStakerRewards[staker][virtualId]; - uint256 total = 0; - for (uint256 i = claim.rewardCount; i < count; i++) { - total += _getClaimableStakerRewardsAt( - i, - virtualId, - staker, - stakingAddress - ); - } - - return total; - } - - function _getClaimableValidatorRewardsAt( - uint256 pos, - uint256 virtualId, - address validator - ) internal view returns (uint256) { - Reward memory reward = getReward(virtualId, SafeCast.toUint32(pos)); - MainReward memory mainReward = getMainReward(reward.mainIndex); - RewardSettingsCheckpoints.RewardSettings - memory rewardSettings = getPastRewardSettings( - mainReward.blockNumber - ); - - uint256 validatorGroupRewards = _validatorRewards[validator][reward.id]; - - return - (validatorGroupRewards * - (DENOMINATOR - uint256(rewardSettings.stakerShares))) / - DENOMINATOR; - } - - function _getClaimableValidatorRewards( - address validator, - uint256 virtualId - ) internal view returns (uint256) { - uint256 count = rewardCount(virtualId); - if (count == 0) { - return 0; - } - - Claim memory claim = _claimedValidatorRewards[validator][virtualId]; - uint256 total = 0; - for (uint256 i = claim.rewardCount; i < count; i++) { - total += _getClaimableValidatorRewardsAt(i, virtualId, validator); - } - - return total; - } - - function getChildrenRewards(uint256 nftId) public view returns (uint256) { - uint256 childrenAmount = 0; - uint256[] memory children = IContributionNft(contributionNft) - .getChildren(nftId); - - ServiceReward memory childReward; - for (uint256 i = 0; i < children.length; i++) { - childReward = getServiceReward(children[i]); - childrenAmount += (childReward.parentAmount - - childReward.totalClaimedParent); - } - return childrenAmount; - } - - function _getClaimableServiceRewards( - uint256 nftId - ) internal view returns (uint256 total) { - ServiceReward memory serviceReward = getServiceReward(nftId); - total = serviceReward.amount - serviceReward.totalClaimed; - uint256 childrenAmount = getChildrenRewards(nftId); - total += childrenAmount; - } - - // ---------------- - // Functions to claim rewards - // ---------------- - function _claimStakerRewards( - address account, - uint256 virtualId - ) internal noReentrant { - uint256 amount = _getClaimableStakerRewards(account, virtualId); - if (amount == 0) { - return; - } - - uint256 count = rewardCount(virtualId); - - Claim storage claim = _claimedStakerRewards[account][virtualId]; - claim.rewardCount = SafeCast.toUint32(count); - claim.totalClaimed += amount; - emit StakerRewardClaimed(virtualId, amount, account); - - IERC20(rewardToken).safeTransfer(account, amount); - } - - function _claimValidatorRewards(uint256 virtualId) internal noReentrant { - address account = _msgSender(); - - uint256 amount = _getClaimableValidatorRewards(account, virtualId); - if (amount == 0) { - return; - } - - uint256 count = rewardCount(virtualId); - - Claim storage claim = _claimedValidatorRewards[account][virtualId]; - claim.rewardCount = SafeCast.toUint32(count); - claim.totalClaimed += amount; - emit ValidatorRewardClaimed(virtualId, amount, account); - - IERC20(rewardToken).safeTransfer(account, amount); - } - - function withdrawProtocolRewards(address recipient) external onlyGov { - require(protocolRewards > 0, "No protocol rewards"); - IERC20(rewardToken).safeTransfer(recipient, protocolRewards); - protocolRewards = 0; - } - - function withdrawValidatorPoolRewards(address recipient) external onlyGov { - require(validatorPoolRewards > 0, "No validator pool rewards"); - IERC20(rewardToken).safeTransfer(recipient, validatorPoolRewards); - validatorPoolRewards = 0; - } - - function getServiceReward( - uint256 nftId - ) public view returns (ServiceReward memory) { - return _serviceRewards[nftId]; - } - - function _claimServiceRewards(uint256 nftId) internal { - address account = _msgSender(); - require( - IERC721(contributionNft).ownerOf(nftId) == account, - "Not NFT owner" - ); - - ServiceReward storage serviceReward = _serviceRewards[nftId]; - uint256 total = (serviceReward.amount - serviceReward.totalClaimed); - - serviceReward.totalClaimed += total; - - // Claim children rewards - uint256[] memory children = IContributionNft(contributionNft) - .getChildren(nftId); - - uint256 totalChildrenAmount; - uint256 childAmount; - for (uint256 i = 0; i < children.length; i++) { - ServiceReward storage childReward = _serviceRewards[children[i]]; - - childAmount = (childReward.parentAmount - - childReward.totalClaimedParent); - - if (childAmount > 0) { - childReward.totalClaimedParent += childAmount; - total += childAmount; - totalChildrenAmount += childAmount; - } - } - - if (total == 0) { - return; - } - - IERC20(rewardToken).safeTransfer(account, total); - emit ServiceRewardsClaimed(nftId, account, total, totalChildrenAmount); - } - - function getTotalClaimableRewards( - address account, - uint256[] memory virtualIds, - uint256[] memory contributionNftIds - ) public view returns (uint256) { - uint256 total = 0; - for (uint256 i = 0; i < virtualIds.length; i++) { - total += - _getClaimableStakerRewards(account, virtualIds[i]) + - _getClaimableValidatorRewards(account, virtualIds[i]); - } - for (uint256 i = 0; i < contributionNftIds.length; i++) { - total += _getClaimableServiceRewards(contributionNftIds[i]); - } - return total; - } - - function claimAllRewards( - uint256[] memory virtualIds, - uint256[] memory contributionNftIds - ) public { - address account = _msgSender(); - for (uint256 i = 0; i < virtualIds.length; i++) { - _claimStakerRewards(account, virtualIds[i]); - _claimValidatorRewards(virtualIds[i]); - } - - for (uint256 i = 0; i < contributionNftIds.length; i++) { - _claimServiceRewards(contributionNftIds[i]); - } - } - - // ---------------- - // Manage parameters - // ---------------- - - function setRewardSettings( - uint16 protocolShares_, - uint16 contributorShares_, - uint16 stakerShares_, - uint16 parentShares_, - uint256 stakeThreshold_ - ) public onlyGov { - _rewardSettings.push( - SafeCast.toUint32(block.number), - RewardSettingsCheckpoints.RewardSettings( - protocolShares_, - contributorShares_, - stakerShares_, - parentShares_, - stakeThreshold_ - ) - ); - - emit RewardSettingsUpdated( - protocolShares_, - contributorShares_, - stakerShares_, - parentShares_, - stakeThreshold_ - ); - } - - function updateRefContracts( - address rewardToken_, - address agentNft_, - address contributionNft_, - address serviceNft_ - ) external onlyGov { - rewardToken = rewardToken_; - agentNft = agentNft_; - contributionNft = contributionNft_; - serviceNft = serviceNft_; - - emit RefContractsUpdated( - rewardToken_, - agentNft_, - contributionNft_, - serviceNft_ - ); - } + // function initialize( + // address rewardToken_, + // address agentNft_, + // address contributionNft_, + // address serviceNft_, + // RewardSettingsCheckpoints.RewardSettings memory settings_ + // ) external initializer { + // rewardToken = rewardToken_; + // agentNft = agentNft_; + // contributionNft = contributionNft_; + // serviceNft = serviceNft_; + // _rewardSettings.push(0, settings_); + // _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + // _nextRewardId = 1; + // } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index 39b50a8..7a636cd 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -38,9 +38,9 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { event NewPersona( uint256 virtualId, address token, - address veToken, address dao, address tba, + address veToken, address lp ); event NewApplication(uint256 id); @@ -259,7 +259,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { application.symbol, initialSupply ); - + // C2 address lp = IAgentToken(token).liquidityPools()[0]; IERC20(assetToken).transfer(token, initialAmount); @@ -272,7 +272,6 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { lp, application.proposer ); - // C4 string memory daoName = string.concat(application.name, " DAO"); @@ -286,17 +285,17 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); // C5 - uint256 virtualId = IAgentNft(nft).nextVirtualId(); - IAgentNft(nft).mint( - virtualId, - _vault, - application.tokenURI, - dao, - application.proposer, - application.cores, - lp, - token - ); + uint256 virtualId = 1;//IAgentNft(nft).nextVirtualId(); + // IAgentNft(nft).mint( + // virtualId, + // _vault, + // application.tokenURI, + // dao, + // application.proposer, + // application.cores, + // lp, + // token + // ); application.virtualId = virtualId; // C6 @@ -320,7 +319,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { application.proposer, defaultDelegatee ); - + emit NewPersona(virtualId, token, veToken, dao, tbaAddress, lp); } @@ -398,10 +397,12 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { function setImplementations( address token, + address veToken, address dao ) public onlyRole(DEFAULT_ADMIN_ROLE) { tokenImplementation = token; daoImplementation = dao; + veTokenImplementation = veToken; } function setMinter(address newMinter) public onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/contracts/virtualPersona/AgentNft.sol b/contracts/virtualPersona/AgentNftV2.sol similarity index 94% rename from contracts/virtualPersona/AgentNft.sol rename to contracts/virtualPersona/AgentNftV2.sol index d082a99..570bce5 100644 --- a/contracts/virtualPersona/AgentNft.sol +++ b/contracts/virtualPersona/AgentNftV2.sol @@ -15,7 +15,7 @@ import "./CoreRegistry.sol"; import "./ValidatorRegistry.sol"; import "./IAgentDAO.sol"; -contract AgentNft is +contract AgentNftV2 is IAgentNft, Initializable, ERC721Upgradeable, @@ -50,9 +50,9 @@ contract AgentNft is address private _serviceNft; // V2 Storage - bytes32 public constant ADMIN_ROLE = - keccak256("ADMIN_ROLE"); + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); mapping(uint256 => bool) private _blacklists; + mapping(uint256 => VirtualLP) public virtualLPs; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -104,9 +104,12 @@ contract AgentNft is info.coreTypes = coreTypes; info.founder = founder; IERC5805 daoToken = GovernorVotes(theDAO).token(); - info.veToken = address(daoToken); info.token = token; - info.pool = pool; + + VirtualLP storage lp = virtualLPs[virtualId]; + lp.pool = pool; + lp.veToken = address(daoToken); + _stakingTokenToVirtualId[address(daoToken)] = virtualId; _addValidator(virtualId, founder); _initValidatorScore(virtualId, founder); @@ -125,6 +128,12 @@ contract AgentNft is return virtualInfos[virtualId]; } + function virtualLP( + uint256 virtualId + ) public view returns (VirtualLP memory) { + return virtualLPs[virtualId]; + } + // Get VIRTUAL ID of a staking token function stakingTokenToVirtualId( address stakingToken @@ -267,7 +276,10 @@ contract AgentNft is return _blacklists[virtualId]; } - function setBlacklist(uint256 virtualId, bool value) public onlyRole(ADMIN_ROLE) { + function setBlacklist( + uint256 virtualId, + bool value + ) public onlyRole(ADMIN_ROLE) { _blacklists[virtualId] = value; } } diff --git a/contracts/virtualPersona/IAgentNft.sol b/contracts/virtualPersona/IAgentNft.sol index 6f4fa1d..c73b602 100644 --- a/contracts/virtualPersona/IAgentNft.sol +++ b/contracts/virtualPersona/IAgentNft.sol @@ -10,12 +10,15 @@ interface IAgentNft is IValidatorRegistry { address founder; address tba; // Token Bound Address uint8[] coreTypes; - address pool; // Liquidity pool for the agent - address veToken; // Voting escrow token } event CoresUpdated(uint256 virtualId, uint8[] coreTypes); + struct VirtualLP { + address pool; // Liquidity pool for the agent + address veToken; // Voting escrow token + } + function mint( uint256 id, address to, diff --git a/hardhat.config.js b/hardhat.config.js index ad86f1b..447d00d 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -4,6 +4,7 @@ require("@nomicfoundation/hardhat-toolbox"); require("hardhat-deploy"); require("@openzeppelin/hardhat-upgrades"); require("@fireblocks/hardhat-fireblocks"); + const { ApiBaseUrl } = require("@fireblocks/fireblocks-web3-provider"); module.exports = { diff --git a/scripts/v2/migrateV2.ts b/scripts/v2/migrateV2.ts index a121f6e..05526ad 100644 --- a/scripts/v2/migrateV2.ts +++ b/scripts/v2/migrateV2.ts @@ -1,15 +1,78 @@ import { ethers, upgrades } from "hardhat"; -async function migrateImplementations(){ - +const adminSigner = new ethers.Wallet( + process.env.ADMIN_PRIVATE_KEY, + ethers.provider +); + +async function upgradeFactory(implementations) { + const AgentFactory = await ethers.getContractFactory("AgentFactoryV2"); + const factory = await upgrades.upgradeProxy( + process.env.VIRTUAL_FACTORY, + AgentFactory + ); + await factory.setImplementations( + implementations.tokenImpl.target, + implementations.veTokenImpl.target, + implementations.daoImpl.target + ); + console.log("Upgraded FactoryV2", factory.target); +} + +async function importContract() { + const ContractFactory = await ethers.getContractFactory( + "AgentNftV2", + adminSigner + ); + await upgrades.forceImport( + "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3", + ContractFactory + ); +} + +async function deployAgentNft() { + const nft = await ethers.deployContract("AgentNftV2"); + console.log("AgentNft deployed to:", nft.target); +} + +async function updateAgentNftRoles() { + const nft = await ethers.getContractAt( + "AgentNftV2", + process.env.VIRTUAL_NFT, + adminSigner + ); + await nft.grantRole(ethers.id("ADMIN_ROLE"), process.env.ADMIN); + console.log("Updated Admin Role"); +} + +async function upgradeAgentNft() { + const Factory = await ethers.getContractFactory("AgentNftV2", adminSigner); + const factory = await upgrades.upgradeProxy(process.env.VIRTUAL_NFT, Factory); + console.log("Upgraded AgentNftV2", factory.target); +} + +async function deployImplementations() { + const daoImpl = await ethers.deployContract("AgentDAO"); + await daoImpl.waitForDeployment(); + console.log("AgentDAO deployed to:", daoImpl.target); + + const tokenImpl = await ethers.deployContract("AgentToken"); + await tokenImpl.waitForDeployment(); + console.log("AgentToken deployed to:", tokenImpl.target); + + const veTokenImpl = await ethers.deployContract("AgentVeToken"); + await veTokenImpl.waitForDeployment(); + console.log("AgentVeToken deployed to:", veTokenImpl.target); + + return { daoImpl, tokenImpl, veTokenImpl }; } (async () => { try { - // Deploy impl - const Contract = await ethers.getContractFactory("AgentRewardV2"); - const contract = await upgrades.upgradeProxy(process.env.PERSONA_REWARD, Contract); - console.log("Upgraded", contract.target) + // const implementations = await deployImplementations(); + // await upgradeFactory(implementations); + //await deployAgentNft(); + await upgradeAgentNft(); } catch (e) { console.log(e); } From d11d4588dab6bf3c551d67c94d760c71e0a5a355 Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 6 Jun 2024 16:46:26 +0800 Subject: [PATCH 14/30] fixed virtual genesis bug --- contracts/virtualPersona/AgentDAO.sol | 5 +- contracts/virtualPersona/AgentFactory.sol | 24 +-- test/agentDAO.js | 2 +- test/virtualGenesis.js | 184 ++++++---------------- 4 files changed, 68 insertions(+), 147 deletions(-) diff --git a/contracts/virtualPersona/AgentDAO.sol b/contracts/virtualPersona/AgentDAO.sol index e3e8442..715b82c 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -229,7 +229,10 @@ contract AgentDAO is ProposalState currentState = super.state(proposalId); if (currentState == ProposalState.Active) { (, uint256 forVotes, ) = proposalVotes(proposalId); - if (forVotes == token().getPastTotalSupply(proposalSnapshot(proposalId))) { + if ( + forVotes == + token().getPastTotalSupply(proposalSnapshot(proposalId)) + ) { return ProposalState.Succeeded; } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index 7a636cd..4235268 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -285,17 +285,17 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); // C5 - uint256 virtualId = 1;//IAgentNft(nft).nextVirtualId(); - // IAgentNft(nft).mint( - // virtualId, - // _vault, - // application.tokenURI, - // dao, - // application.proposer, - // application.cores, - // lp, - // token - // ); + uint256 virtualId = IAgentNft(nft).nextVirtualId(); + IAgentNft(nft).mint( + virtualId, + _vault, + application.tokenURI, + dao, + application.proposer, + application.cores, + lp, + token + ); application.virtualId = virtualId; // C6 @@ -320,7 +320,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { defaultDelegatee ); - emit NewPersona(virtualId, token, veToken, dao, tbaAddress, lp); + emit NewPersona(virtualId, token, dao, tbaAddress, veToken, lp); } function _createNewDAO( diff --git a/test/agentDAO.js b/test/agentDAO.js index d34690a..ac28b7d 100644 --- a/test/agentDAO.js +++ b/test/agentDAO.js @@ -83,7 +83,7 @@ describe("AgentDAO", function () { ); await virtualToken.waitForDeployment(); - const AgentNft = await ethers.getContractFactory("AgentNft"); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); const contribution = await upgrades.deployProxy( diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index 45fa060..8369ce8 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -39,13 +39,13 @@ describe("AgentFactoryV2", function () { }; const getAccounts = async () => { - const [deployer, ipVault, founder, poorMan, trader] = + const [deployer, ipVault, founder, poorMan, trader, treasury] = await ethers.getSigners(); - return { deployer, ipVault, founder, poorMan, trader }; + return { deployer, ipVault, founder, poorMan, trader, treasury }; }; async function deployBaseContracts() { - const { deployer, ipVault } = await getAccounts(); + const { deployer, ipVault, treasury } = await getAccounts(); const virtualToken = await ethers.deployContract( "VirtualToken", @@ -54,7 +54,7 @@ describe("AgentFactoryV2", function () { ); await virtualToken.waitForDeployment(); - const AgentNft = await ethers.getContractFactory("AgentNft"); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); const contribution = await upgrades.deployProxy( @@ -105,8 +105,21 @@ describe("AgentFactoryV2", function () { ]); await minter.waitForDeployment(); await agentFactory.setMinter(minter.target); - await agentFactory.setMaturityDuration(86400 * 365 * 10); // 100years + await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); + await agentFactory.setTokenAdmin(deployer.address); + await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION + ); + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address + ); + await agentFactory.setDefaultDelegatee(deployer.address); return { virtualToken, agentFactory, agentNft }; } @@ -170,7 +183,7 @@ describe("AgentFactoryV2", function () { before(async function () {}); - xit("should be able to propose a new agent", async function () { + it("should be able to propose a new agent", async function () { const { agentFactory, virtualToken } = await loadFixture( deployBaseContracts ); @@ -206,7 +219,7 @@ describe("AgentFactoryV2", function () { expect(id).to.be.equal(1n); }); - xit("should deny new Persona proposal when insufficient asset token", async function () { + it("should deny new Persona proposal when insufficient asset token", async function () { const { agentFactory } = await loadFixture(deployBaseContracts); const { poorMan } = await getAccounts(); await expect( @@ -225,7 +238,7 @@ describe("AgentFactoryV2", function () { ).to.be.revertedWith("Insufficient asset token"); }); - xit("should allow application execution by proposer", async function () { + it("should allow application execution by proposer", async function () { const { applicationId, agentFactory, virtualToken } = await loadFixture( deployWithApplication ); @@ -236,23 +249,21 @@ describe("AgentFactoryV2", function () { // Check genesis components // C1: Agent Token - // C2: LP Pool + // C2: LP Pool + Initial liquidity // C3: Agent veToken // C4: Agent DAO // C5: Agent NFT // C6: TBA - // C7: Mint initial Agent tokens - // C8: Provide liquidity - // C9: Stake liquidity token to get veToken + // C7: Stake liquidity token to get veToken }); - xit("agent component C1: Agent Token", async function () { + it("agent component C1: Agent Token", async function () { const { agent } = await loadFixture(deployWithAgent); const agentToken = await ethers.getContractAt("AgentToken", agent.token); expect(await agentToken.totalSupply()).to.be.equal(PROPOSAL_THRESHOLD); }); - xit("agent component C2: LP Pool", async function () { + it("agent component C2: LP Pool", async function () { const { agent, virtualToken } = await loadFixture(deployWithAgent); const lp = await ethers.getContractAt("IUniswapV2Pair", agent.lp); const t0 = await lp.token0(); @@ -267,7 +278,7 @@ describe("AgentFactoryV2", function () { expect(reserves[1]).to.be.equal(PROPOSAL_THRESHOLD); }); - xit("agent component C3: Agent veToken", async function () { + it("agent component C3: Agent veToken", async function () { const { agent } = await loadFixture(deployWithAgent); const { founder } = await getAccounts(); const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); @@ -277,18 +288,21 @@ describe("AgentFactoryV2", function () { ); }); - xit("agent component C4: Agent DAO", async function () { + it("agent component C4: Agent DAO", async function () { const { agent } = await loadFixture(deployWithAgent); const dao = await ethers.getContractAt("AgentDAO", agent.dao); expect(await dao.token()).to.be.equal(agent.veToken); expect(await dao.name()).to.be.equal(genesisInput.daoName); }); - xit("agent component C5: Agent NFT", async function () { + it("agent component C5: Agent NFT", async function () { const { agent, agentNft } = await loadFixture(deployWithAgent); const virtualInfo = await agentNft.virtualInfo(agent.virtualId); expect(virtualInfo.dao).to.equal(agent.dao); expect(virtualInfo.coreTypes).to.deep.equal(genesisInput.cores); + const virtualLP = await agentNft.virtualLP(agent.virtualId); + expect(virtualLP.pool).to.be.equal(agent.lp); + expect(virtualLP.veToken).to.be.equal(agent.veToken); expect(await agentNft.tokenURI(agent.virtualId)).to.equal( genesisInput.tokenURI @@ -297,7 +311,7 @@ describe("AgentFactoryV2", function () { expect(virtualInfo.tba).to.equal(agent.tba); }); - xit("agent component C6: TBA", async function () { + it("agent component C6: TBA", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -323,7 +337,7 @@ describe("AgentFactoryV2", function () { expect(balance).to.be.equal(amount); }); - xit("agent component C7: Mint initial Agent tokens", async function () { + it("agent component C7: Mint initial Agent tokens", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -336,103 +350,7 @@ describe("AgentFactoryV2", function () { ); }); - xit("agent component C8: Provide liquidity", async function () { - // TBA means whoever owns the NFT can move the account assets - // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out - const { agent, agentNft, virtualToken } = await loadFixture( - deployWithAgent - ); - const agentToken = await ethers.getContractAt("AgentToken", agent.token); - const { trader } = await getAccounts(); - // Swap - const router = await ethers.getContractAt( - "IUniswapV2Router02", - process.env.UNISWAP_ROUTER - ); - const amountToBuy = parseEther("90"); - const capital = parseEther("100"); - await virtualToken.mint(trader.address, capital); - await virtualToken - .connect(trader) - .approve(process.env.UNISWAP_ROUTER, capital); - - await router - .connect(trader) - .swapTokensForExactTokens( - amountToBuy, - capital, - [virtualToken.target, agent.token], - trader.address, - Math.floor(new Date().getTime() / 1000 + 6000) - ); - expect( - parseFloat(formatEther(await virtualToken.balanceOf(trader.address))) - ).to.be.at.most(10); - expect( - parseFloat(formatEther(await agentToken.balanceOf(trader.address))) - ).to.be.equal(90); - }); - - xit("agent component C9: Stake liquidity token to get veToken", async function () { - // TBA means whoever owns the NFT can move the account assets - // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out - const { agent, agentNft, virtualToken } = await loadFixture( - deployWithAgent - ); - const { trader } = await getAccounts(); - const router = await ethers.getContractAt( - "IUniswapV2Router02", - process.env.UNISWAP_ROUTER - ); - const agentToken = await ethers.getContractAt("AgentToken", agent.token); - - // Buy tokens - const amountToBuy = parseEther("90"); - const capital = parseEther("200"); - await virtualToken.mint(trader.address, capital); - await virtualToken - .connect(trader) - .approve(process.env.UNISWAP_ROUTER, capital); - - await router - .connect(trader) - .swapTokensForExactTokens( - amountToBuy, - capital, - [virtualToken.target, agent.token], - trader.address, - Math.floor(new Date().getTime() / 1000 + 6000) - ); - //// - // Start providing liquidity - const lpToken = await ethers.getContractAt("ERC20", agent.lp); - expect(await lpToken.balanceOf(trader.address)).to.be.equal(0n); - await agentToken - .connect(trader) - .approve(process.env.UNISWAP_ROUTER, amountToBuy); - await virtualToken - .connect(trader) - .approve(process.env.UNISWAP_ROUTER, capital); - await router - .connect(trader) - .addLiquidity( - agentToken.target, - virtualToken.target, - amountToBuy, - capital, - 0, - 0, - trader.address, - Math.floor(new Date().getTime() / 1000 + 6000) - ); - - expect( - parseFloat(formatEther(await lpToken.balanceOf(trader.address))) - ).to.be.at.least(90); - expect(await agentToken.balanceOf(trader.address)).to.be.equal(0n); - }); - - xit("should allow staking on public agent", async function () { + it("should allow staking on public agent", async function () { // Need to provide LP first const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent @@ -469,17 +387,18 @@ describe("AgentFactoryV2", function () { expect(await lpToken.balanceOf(trader.address)).to.be.equal(0n); await agentToken .connect(trader) - .approve(process.env.UNISWAP_ROUTER, amountToBuy); + .approve(process.env.UNISWAP_ROUTER, parseEther("10000000")); await virtualToken .connect(trader) - .approve(process.env.UNISWAP_ROUTER, capital); + .approve(process.env.UNISWAP_ROUTER, parseEther("10000000")); + await router .connect(trader) .addLiquidity( agentToken.target, virtualToken.target, - amountToBuy, - capital, + await agentToken.balanceOf(trader.address), + await virtualToken.balanceOf(trader.address), 0, 0, trader.address, @@ -493,7 +412,7 @@ describe("AgentFactoryV2", function () { .stake(parseEther("10"), trader.address, poorMan.address); }); - xit("should deny staking on private agent", async function () { + it("should deny staking on private agent", async function () { // Need to provide LP first const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent @@ -537,8 +456,8 @@ describe("AgentFactoryV2", function () { .addLiquidity( agentToken.target, virtualToken.target, - amountToBuy, - capital, + await agentToken.balanceOf(trader.address), + await virtualToken.balanceOf(trader.address), 0, 0, trader.address, @@ -554,7 +473,7 @@ describe("AgentFactoryV2", function () { ).to.be.revertedWith("Staking is disabled for private agent"); }); - xit("should be able to set new validator and able to update score", async function () { + it("should be able to set new validator and able to update score", async function () { // Need to provide LP first const { agent, agentNft } = await loadFixture(deployWithAgent); const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); @@ -601,7 +520,7 @@ describe("AgentFactoryV2", function () { ).to.be.equal(1n); }); - xit("should be able to set new validator after created proposals and have correct score", async function () { + it("should be able to set new validator after created proposals and have correct score", async function () { const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent ); @@ -647,8 +566,8 @@ describe("AgentFactoryV2", function () { .addLiquidity( agentToken.target, virtualToken.target, - amountToBuy, - capital, + await agentToken.balanceOf(trader.address), + await virtualToken.balanceOf(trader.address), 0, 0, trader.address, @@ -687,7 +606,7 @@ describe("AgentFactoryV2", function () { expect(newScore).to.be.equal(2n); }); - xit("should allow withdrawal", async function () { + it("should allow withdrawal", async function () { const { applicationId, agentFactory, virtualToken } = await loadFixture( deployWithApplication ); @@ -698,7 +617,7 @@ describe("AgentFactoryV2", function () { ); }); - xit("should lock initial LP", async function () { + it("should lock initial LP", async function () { const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent ); @@ -717,7 +636,7 @@ describe("AgentFactoryV2", function () { const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent ); - const {founder, deployer} = await getAccounts() + const { founder, deployer } = await getAccounts(); // Assign admin role await agentNft.grantRole(await agentNft.ADMIN_ROLE(), deployer); const agentVeToken = await ethers.getContractAt( @@ -728,8 +647,7 @@ describe("AgentFactoryV2", function () { expect(await agentNft.isBlacklisted(agent.virtualId)).to.be.equal(true); await expect(agentVeToken.setMatureAt(0)).to.not.be.reverted; - await expect( - agentVeToken.connect(founder).withdraw(parseEther("10")) - ).to.not.be.reverted; + await expect(agentVeToken.connect(founder).withdraw(parseEther("10"))).to + .not.be.reverted; }); }); From bfa2ae99c72d279c1953d4cd9182e8a557fc1a67 Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 6 Jun 2024 16:59:35 +0800 Subject: [PATCH 15/30] make initialLock public --- contracts/virtualPersona/AgentVeToken.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index 2f17ad7..2e17b65 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -19,7 +19,7 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { address public agentNft; uint256 public matureAt; // The timestamp when the founder can withdraw the tokens bool public canStake; // To control private/public agent mode - uint256 private initialLock; // Initial locked amount + uint256 public initialLock; // Initial locked amount constructor() { _disableInitializers(); From 2012456456aa1ed280dc9db2b1b8e37624f88c87 Mon Sep 17 00:00:00 2001 From: kw Date: Mon, 10 Jun 2024 11:20:36 +0800 Subject: [PATCH 16/30] reward wip --- contracts/AgentRewardV2.sol | 211 ++++++++++++++++--- contracts/IAgentReward.sol | 46 ++-- contracts/libs/RewardSettingsCheckpoints.sol | 7 +- contracts/virtualPersona/AgentDAO.sol | 7 + contracts/virtualPersona/AgentFactory.sol | 33 +-- contracts/virtualPersona/AgentVeToken.sol | 24 ++- contracts/virtualPersona/IAgentDAO.sol | 2 + contracts/virtualPersona/IAgentNft.sol | 4 + contracts/virtualPersona/IAgentVeToken.sol | 7 +- scripts/v2/migrateV2.ts | 2 +- 10 files changed, 264 insertions(+), 79 deletions(-) diff --git a/contracts/AgentRewardV2.sol b/contracts/AgentRewardV2.sol index 4e0ab82..f4bee88 100644 --- a/contracts/AgentRewardV2.sol +++ b/contracts/AgentRewardV2.sol @@ -10,18 +10,25 @@ import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "./virtualPersona/IAgentNft.sol"; import "./virtualPersona/IAgentToken.sol"; +import "./virtualPersona/IAgentDAO.sol"; +import "./virtualPersona/IAgentVeToken.sol"; import "./libs/RewardSettingsCheckpoints.sol"; import "./contribution/IContributionNft.sol"; import "./contribution/IServiceNft.sol"; import "./libs/TokenSaver.sol"; import "./IAgentReward.sol"; -contract AgentRewardV2 { +contract AgentRewardV2 is + IAgentReward, + Initializable, + AccessControl, + TokenSaver +{ using Math for uint256; using SafeERC20 for IERC20; using RewardSettingsCheckpoints for RewardSettingsCheckpoints.Trace; - uint48 private _nextRewardId; + uint48 private _nextAgentRewardId; uint256 public constant DENOMINATOR = 10000; bytes32 public constant GOV_ROLE = keccak256("GOV_ROLE"); @@ -29,15 +36,22 @@ contract AgentRewardV2 { // Referencing contracts address public rewardToken; address public agentNft; - address public contributionNft; - address public serviceNft; - // modifier onlyGov() { - // if (!hasRole(GOV_ROLE, _msgSender())) { - // revert NotGovError(); - // } - // _; - // } + // Rewards checkpoints, split into Master reward and Virtual shares + Reward[] private _rewards; + mapping(uint256 virtualId => AgentReward[]) private _agentRewards; + + RewardSettingsCheckpoints.Trace private _rewardSettings; + + // Rewards ledger + uint256 public protocolRewards; + + modifier onlyGov() { + if (!hasRole(GOV_ROLE, _msgSender())) { + revert NotGovError(); + } + _; + } bool internal locked; @@ -48,19 +62,166 @@ contract AgentRewardV2 { locked = false; } - // function initialize( - // address rewardToken_, - // address agentNft_, - // address contributionNft_, - // address serviceNft_, - // RewardSettingsCheckpoints.RewardSettings memory settings_ - // ) external initializer { - // rewardToken = rewardToken_; - // agentNft = agentNft_; - // contributionNft = contributionNft_; - // serviceNft = serviceNft_; - // _rewardSettings.push(0, settings_); - // _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); - // _nextRewardId = 1; - // } + function initialize( + address rewardToken_, + address agentNft_, + RewardSettingsCheckpoints.RewardSettings memory settings_ + ) external initializer { + rewardToken = rewardToken_; + agentNft = agentNft_; + _rewardSettings.push(0, settings_); + _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + _nextAgentRewardId = 1; + } + + function getRewardSettings() + public + view + returns (RewardSettingsCheckpoints.RewardSettings memory) + { + return _rewardSettings.latest(); + } + + function getPastRewardSettings( + uint32 timepoint + ) public view returns (RewardSettingsCheckpoints.RewardSettings memory) { + uint32 currentTimepoint = SafeCast.toUint32(block.number); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } + return _rewardSettings.upperLookupRecent(timepoint); + } + + function getReward(uint48 pos) public view returns (Reward memory) { + return _rewards[pos]; + } + + function getAgentReward( + uint256 virtualId, + uint48 pos + ) public view returns (AgentReward memory) { + return _agentRewards[virtualId][pos]; + } + + function agentRewardCount(uint256 virtualId) public view returns (uint256) { + return _agentRewards[virtualId].length; + } + + function rewardCount() public view returns (uint256) { + return _rewards.length; + } + + // ---------------- + // Helper functions + // ---------------- + function getTotalStaked( + uint256[] memory virtualIds + ) internal view returns (uint256 totalStaked) { + uint length = virtualIds.length; + for (uint i = 0; i < length; i++) { + uint256 virtualId = virtualIds[i]; + uint256 staked = IERC20( + IAgentNft(agentNft).virtualLP(virtualId).veToken + ).totalSupply(); + totalStaked += staked; + } + } + + // ---------------- + // Distribute rewards + // ---------------- + + function distributeRewards( + uint256 amount, + uint256 totalStaked, + uint256[] memory virtualIds, + bool shouldShareWithProtocol + ) public onlyGov { + require(amount > 0, "Invalid amount"); + + IERC20(rewardToken).safeTransferFrom( + _msgSender(), + address(this), + amount + ); + + RewardSettingsCheckpoints.RewardSettings + memory settings = getRewardSettings(); + + uint256 protocolAmount = shouldShareWithProtocol + ? _distributeProtocolRewards(amount) + : 0; + + uint256 balance = amount - protocolAmount; + + uint32 rewardIndex = SafeCast.toUint32(_rewards.length - 1); + + _rewards.push(Reward(block.number, balance, totalStaked, virtualIds)); + + emit NewReward(rewardIndex, balance, protocolAmount, totalStaked); + + // We don't expect too many virtuals here, the loop should not exceed gas limit + uint length = virtualIds.length; + for (uint i = 0; i < length; i++) { + uint256 virtualId = virtualIds[i]; + _distributeAgentReward( + virtualId, + rewardIndex, + balance, + totalStaked, + settings + ); + } + } + + function _distributeAgentReward( + uint256 virtualId, + uint48 rewardIndex, + uint256 totalAmount, + uint256 totalStaked, + RewardSettingsCheckpoints.RewardSettings memory settings + ) private { + uint48 agentRewardId = _nextAgentRewardId++; + uint256 staked = IERC20( + IAgentNft(agentNft).virtualLP(virtualId).veToken + ).totalSupply(); + + uint256 rewardAmount = (staked * totalAmount) / totalStaked; + + uint256 stakerAmount = (rewardAmount * settings.stakerShares) / + DENOMINATOR; + + uint256 totalScore = IAgentDAO(IAgentNft(agentNft).virtualInfo(virtualId).dao) + .totalScore(); + + _agentRewards[virtualId].push( + AgentReward( + agentRewardId, + rewardIndex, + stakerAmount, + rewardAmount - stakerAmount, + totalScore + ) + ); + + emit NewAgentReward( + virtualId, + agentRewardId, + rewardIndex, + stakerAmount, + rewardAmount - stakerAmount, + totalScore + ); + } + + function _distributeProtocolRewards( + uint256 amount + ) private returns (uint256) { + RewardSettingsCheckpoints.RewardSettings + memory rewardSettings = _rewardSettings.latest(); + uint256 protocolShares = (amount * rewardSettings.protocolShares) / + DENOMINATOR; + protocolRewards += protocolShares; + return protocolShares; + } } diff --git a/contracts/IAgentReward.sol b/contracts/IAgentReward.sol index e0c7e92..180cb64 100644 --- a/contracts/IAgentReward.sol +++ b/contracts/IAgentReward.sol @@ -2,23 +2,20 @@ pragma solidity ^0.8.20; interface IAgentReward { - struct MainReward { - uint32 blockNumber; + struct Reward { + uint256 blockNumber; uint256 amount; - uint256 agentCount; uint256 totalStaked; + uint256[] virtualIds; } - // Virtual specific reward, the amount will be shared between validator pool and contributor pool - // Validator pool will be shared by validators and stakers - // Contributor pool will be shared by contribution NFT holders - struct Reward { + // Agent specific reward, the amount will be shared between stakers and validators + struct AgentReward { uint48 id; - uint32 mainIndex; - uint256 totalStaked; + uint48 rewardIndex; + uint256 stakerAmount; uint256 validatorAmount; - uint256 contributorAmount; - uint256 coreAmount; // Rewards per core + uint256 totalScore; } struct Claim { @@ -26,7 +23,23 @@ interface IAgentReward { uint32 rewardCount; // Track number of reward blocks claimed to avoid reclaiming } - struct ServiceReward { + event NewReward( + uint48 pos, + uint256 amount, + uint256 protocolAmount, + uint256 totalStaked + ); + + event NewAgentReward( + uint256 indexed virtualId, + uint48 id, + uint48 rewardIndex, + uint256 stakerAmount, + uint256 validatorAmount, + uint256 totalScore + ); + + /*struct ServiceReward { uint256 impact; uint256 amount; uint256 parentAmount; @@ -34,12 +47,7 @@ interface IAgentReward { uint256 totalClaimedParent; } - event NewMainReward( - uint32 indexed pos, - uint256 amount, - uint256 agentCount, - uint256 totalStaked - ); + event RewardSettingsUpdated( uint16 protocolShares, @@ -88,7 +96,7 @@ interface IAgentReward { ); event DatasetRewardsClaimed(uint256 nftId, address account, uint256 total); - +*/ error ERC5805FutureLookup(uint256 timepoint, uint32 clock); error NotGovError(); diff --git a/contracts/libs/RewardSettingsCheckpoints.sol b/contracts/libs/RewardSettingsCheckpoints.sol index 04bb8a4..ef9e19d 100644 --- a/contracts/libs/RewardSettingsCheckpoints.sol +++ b/contracts/libs/RewardSettingsCheckpoints.sol @@ -13,10 +13,7 @@ library RewardSettingsCheckpoints { struct RewardSettings { uint16 protocolShares; - uint16 contributorShares; uint16 stakerShares; - uint16 parentShares; // Optional rewards for contribution's parent - uint256 stakeThreshold; // Each VIRTUAL will require minimum amount of staked tokens to be considered for rewards } struct Checkpoint { @@ -62,7 +59,7 @@ library RewardSettingsCheckpoints { uint256 pos = self._checkpoints.length; return pos == 0 - ? RewardSettings(0, 0, 0, 0, 0) + ? RewardSettings(0, 0) : self._checkpoints[pos - 1]._value; } @@ -88,7 +85,7 @@ library RewardSettingsCheckpoints { return pos == 0 - ? RewardSettings(0, 0, 0, 0, 0) + ? RewardSettings(0, 0) : self._checkpoints[pos - 1]._value; } diff --git a/contracts/virtualPersona/AgentDAO.sol b/contracts/virtualPersona/AgentDAO.sol index e3e8442..772b96d 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -28,6 +28,8 @@ contract AgentDAO is error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + uint256 private _totalScore; + constructor() { _disableInitializers(); } @@ -164,6 +166,7 @@ contract AgentDAO is ); if (!votedPreviously && hasVoted(proposalId, account)) { + ++_totalScore; _scores[account].push( SafeCast.toUint48(block.number), SafeCast.toUint208(scoreOf(account)) + 1 @@ -235,4 +238,8 @@ contract AgentDAO is } return currentState; } + + function totalScore() public view override returns (uint256) { + return _totalScore; + } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index 7a636cd..64e2209 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -223,7 +223,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); } - function executeApplication(uint256 id) public noReentrant { + function executeApplication(uint256 id, bool canStake) public noReentrant { // This will bootstrap an Agent with following components: // C1: Agent Token // C2: LP Pool + Initial liquidity @@ -270,7 +270,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { string.concat("Staked ", application.name), string.concat("s", application.symbol), lp, - application.proposer + application.proposer, + canStake ); // C4 @@ -285,17 +286,17 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); // C5 - uint256 virtualId = 1;//IAgentNft(nft).nextVirtualId(); - // IAgentNft(nft).mint( - // virtualId, - // _vault, - // application.tokenURI, - // dao, - // application.proposer, - // application.cores, - // lp, - // token - // ); + uint256 virtualId = IAgentNft(nft).nextVirtualId(); + IAgentNft(nft).mint( + virtualId, + _vault, + application.tokenURI, + dao, + application.proposer, + application.cores, + lp, + token + ); application.virtualId = virtualId; // C6 @@ -364,7 +365,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { string memory name, string memory symbol, address stakingAsset, - address founder + address founder, + bool canStake ) internal returns (address instance) { instance = Clones.clone(veTokenImplementation); IAgentVeToken(instance).initialize( @@ -373,7 +375,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { founder, stakingAsset, block.timestamp + maturityDuration, - address(nft) + address(nft), + canStake ); allTokens.push(instance); diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index 2f17ad7..a8f6a03 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -37,20 +37,22 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { } function initialize( - string memory name, - string memory symbol, - address founder_, - address assetToken_, - uint256 matureAt_, - address agentNft_ + string memory _name, + string memory _symbol, + address _founder, + address _assetToken, + uint256 _matureAt, + address _agentNft, + bool _canStake ) external initializer { - __ERC20_init(name, symbol); + __ERC20_init(_name, _symbol); __ERC20Votes_init(); - founder = founder_; - matureAt = matureAt_; - assetToken = assetToken_; - agentNft = agentNft_; + founder = _founder; + matureAt = _matureAt; + assetToken = _assetToken; + agentNft = _agentNft; + canStake = _canStake; } // Stakers have to stake their tokens and delegate to a validator diff --git a/contracts/virtualPersona/IAgentDAO.sol b/contracts/virtualPersona/IAgentDAO.sol index 4136eb0..5c3f098 100644 --- a/contracts/virtualPersona/IAgentDAO.sol +++ b/contracts/virtualPersona/IAgentDAO.sol @@ -17,6 +17,8 @@ interface IAgentDAO { function scoreOf(address account) external view returns (uint256); + function totalScore() external view returns (uint256); + function getPastScore( address account, uint256 timepoint diff --git a/contracts/virtualPersona/IAgentNft.sol b/contracts/virtualPersona/IAgentNft.sol index c73b602..a328465 100644 --- a/contracts/virtualPersona/IAgentNft.sol +++ b/contracts/virtualPersona/IAgentNft.sol @@ -40,6 +40,10 @@ interface IAgentNft is IValidatorRegistry { uint256 virtualId ) external view returns (VirtualInfo memory); + function virtualLP( + uint256 virtualId + ) external view returns (VirtualLP memory); + function totalSupply() external view returns (uint256); function totalStaked(uint256 virtualId) external view returns (uint256); diff --git a/contracts/virtualPersona/IAgentVeToken.sol b/contracts/virtualPersona/IAgentVeToken.sol index dba6f72..c03fd44 100644 --- a/contracts/virtualPersona/IAgentVeToken.sol +++ b/contracts/virtualPersona/IAgentVeToken.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.20; interface IAgentVeToken { function initialize( - string memory name, - string memory symbol, + string memory _name, + string memory _symbol, address _founder, address _assetToken, uint256 _matureAt, - address agentNft_ + address _agentNft, + bool _canStake ) external; function stake( diff --git a/scripts/v2/migrateV2.ts b/scripts/v2/migrateV2.ts index 05526ad..8bcad50 100644 --- a/scripts/v2/migrateV2.ts +++ b/scripts/v2/migrateV2.ts @@ -72,7 +72,7 @@ async function deployImplementations() { // const implementations = await deployImplementations(); // await upgradeFactory(implementations); //await deployAgentNft(); - await upgradeAgentNft(); + //await upgradeAgentNft(); } catch (e) { console.log(e); } From 079817d020b53e9123d97d60d3bc9878da33e862 Mon Sep 17 00:00:00 2001 From: kw Date: Mon, 10 Jun 2024 11:28:25 +0800 Subject: [PATCH 17/30] updated test cases --- test/agentDAO.js | 3 ++- test/contribution.js | 1 + test/delegate.js | 1 + test/rewardsV2.js | 3 ++- test/virtualGenesis.js | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/test/agentDAO.js b/test/agentDAO.js index d34690a..fb95ab9 100644 --- a/test/agentDAO.js +++ b/test/agentDAO.js @@ -17,6 +17,7 @@ const { parseEther, formatEther } = require("ethers"); const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, + false ]); }; @@ -196,7 +197,7 @@ describe("AgentDAO", function () { const { agentFactory, applicationId } = base; const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId); + await agentFactory.connect(founder).executeApplication(applicationId, false); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); diff --git a/test/contribution.js b/test/contribution.js index a493a02..e4110aa 100644 --- a/test/contribution.js +++ b/test/contribution.js @@ -19,6 +19,7 @@ const { const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, + false ]); }; diff --git a/test/delegate.js b/test/delegate.js index 325b989..478de5f 100644 --- a/test/delegate.js +++ b/test/delegate.js @@ -11,6 +11,7 @@ const { const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, + false ]); }; diff --git a/test/rewardsV2.js b/test/rewardsV2.js index 70c016c..229aeec 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -17,6 +17,7 @@ const { parseEther, formatEther } = require("ethers"); const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, + false ]); }; @@ -193,7 +194,7 @@ describe("RewardsV2", function () { const { agentFactory, applicationId } = base; const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId); + await agentFactory.connect(founder).executeApplication(applicationId, false); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index 45fa060..81c11c6 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -147,7 +147,7 @@ describe("AgentFactoryV2", function () { const { agentFactory, applicationId } = base; const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId); + await agentFactory.connect(founder).executeApplication(applicationId, false); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); From 938bbef430109bdd138b854003c15fa725990ec0 Mon Sep 17 00:00:00 2001 From: kw Date: Mon, 10 Jun 2024 14:19:03 +0800 Subject: [PATCH 18/30] wip --- .openzeppelin/base-sepolia.json | 1236 +++++++++++++++++++++ contracts/virtualPersona/AgentFactory.sol | 9 +- scripts/v2/migrateV2.ts | 36 +- scripts/v2/testGenesis.ts | 52 + test/agentDAO.js | 2 +- test/virtualGenesis.js | 90 +- 6 files changed, 1376 insertions(+), 49 deletions(-) create mode 100644 scripts/v2/testGenesis.ts diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json index e5fa923..8fd3b6c 100644 --- a/.openzeppelin/base-sepolia.json +++ b/.openzeppelin/base-sepolia.json @@ -8750,6 +8750,1242 @@ "0x426A2a2fcdA4851E411a5afDa7480263bd02E254", "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3" ] + }, + "7afde3df5370c0b1bba45fed6d45cdfcdc697c63d75ec2667077864a9d339f02": { + "address": "0x6B0A1335080B7860F3e8Da8c8aE98E4AadacB7e0", + "txHash": "0x859482b192575cfed90b27a12f967826d70b2fbad8d0207dd51f7958c1d3ecd9", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)7540_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:23" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:24" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:25" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:26" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:33" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)21854_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:70" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:72" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:83" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:85" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:97" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:98" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:99" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:100" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:107" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)744_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)21825": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)7540_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)21854_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)21854_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)21825", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)7540_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "298223e30fb076efde1b803399978fba144731385ba54bb99bda2f03ed3479a9": { + "address": "0x545247Deb3E0e9A7BE2ee80375484FB5d84d4E19", + "txHash": "0x580b50ce7a4f4e374027e8aa701c7b100ce8075cead142808b836a6002e83f95", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)275_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:23" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:24" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:25" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:26" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:33" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)2321_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:70" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:72" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:83" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:85" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:97" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:98" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:99" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:100" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:107" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)10_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)2292": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)275_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)2321_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)2321_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)2292", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)275_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "de19bf9b2a1efd3de8a1414f3ca6f29c35bebfb33463d1e2c12e2a38bb378550": { + "address": "0xE55bcbed5cF46aCAd261FD913B55880d509F3023", + "txHash": "0xfb9ca5e532d3c0ad4593b9980b50bc221959b47502544822b404a4e79b86c382", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)275_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:23" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:24" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:25" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:26" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:33" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)2321_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:70" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:72" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:83" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:85" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:97" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:98" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:99" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:100" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:107" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)10_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)2292": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)275_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)2321_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)2321_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)2292", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)275_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } } } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index 64e2209..dbbe7dd 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -321,7 +321,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { defaultDelegatee ); - emit NewPersona(virtualId, token, veToken, dao, tbaAddress, lp); + emit NewPersona(virtualId, token, dao, tbaAddress, veToken, lp); } function _createNewDAO( @@ -348,6 +348,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { string memory symbol, uint256 initialSupply ) internal returns (address instance) { + instance = Clones.clone(tokenImplementation); IAgentToken(instance).initialize( [_tokenAdmin, _uniswapRouter, assetToken], @@ -471,4 +472,10 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ) public onlyRole(DEFAULT_ADMIN_ROLE) { defaultDelegatee = newDelegatee; } + + function setAssetToken( + address newToken + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + assetToken = newToken; + } } diff --git a/scripts/v2/migrateV2.ts b/scripts/v2/migrateV2.ts index 8bcad50..9abdcd0 100644 --- a/scripts/v2/migrateV2.ts +++ b/scripts/v2/migrateV2.ts @@ -5,17 +5,35 @@ const adminSigner = new ethers.Wallet( ethers.provider ); -async function upgradeFactory(implementations) { +async function upgradeFactory(implementations, assetToken) { const AgentFactory = await ethers.getContractFactory("AgentFactoryV2"); const factory = await upgrades.upgradeProxy( process.env.VIRTUAL_FACTORY, AgentFactory ); - await factory.setImplementations( - implementations.tokenImpl.target, - implementations.veTokenImpl.target, - implementations.daoImpl.target + if (implementations) { + await factory.setImplementations( + implementations.tokenImpl.target, + implementations.veTokenImpl.target, + implementations.daoImpl.target + ); + } + if (assetToken) { + await factory.setAssetToken(assetToken); + } + await factory.setTokenAdmin(process.env.ADMIN); + await factory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION + ); + await factory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + process.env.TAX_VAULT ); + await factory.setDefaultDelegatee(process.env.ADMIN); console.log("Upgraded FactoryV2", factory.target); } @@ -69,10 +87,10 @@ async function deployImplementations() { (async () => { try { - // const implementations = await deployImplementations(); - // await upgradeFactory(implementations); - //await deployAgentNft(); - //await upgradeAgentNft(); + const implementations = await deployImplementations(); + await upgradeFactory(implementations, process.env.BRIDGED_TOKEN); + await deployAgentNft(); + await upgradeAgentNft(); } catch (e) { console.log(e); } diff --git a/scripts/v2/testGenesis.ts b/scripts/v2/testGenesis.ts new file mode 100644 index 0000000..ff956ff --- /dev/null +++ b/scripts/v2/testGenesis.ts @@ -0,0 +1,52 @@ +import { ethers, upgrades } from "hardhat"; + +const adminSigner = new ethers.Wallet( + process.env.ADMIN_PRIVATE_KEY, + ethers.provider +); + +async function createAgent() { + const factory = await ethers.getContractAt( + "AgentFactoryV2", + process.env.VIRTUAL_FACTORY, + adminSigner + ); + const token = await ethers.getContractAt( + "IERC20", + await factory.assetToken(), + adminSigner + ); + const approveTx = await token.approve(factory.target, await factory.applicationThreshold()); + await approveTx.wait(); + const tx = await factory.proposeAgent( + "KWTest", + "KWT", + "https://azure-rapid-lungfish-73.mypinata.cloud/ipfs/QmQBSxbMjSDPN2aT3jbLbvKbghKMyYVoDZHhdh3SKPba8e", + [0, 1, 2], + "0xce5d1e74b5ac2a84f803ec245ffbeddd6a0b5ef54924229aab241ade1126354e", + "0x55266d75D1a14E4572138116aF39863Ed6596E7F", + 1800n, + 0 + ); + + await tx.wait(); + + const filter = factory.filters.NewApplication; + const events = await factory.queryFilter(filter, -1); + const event = events[0]; + const { id } = event.args; + if (id) { + console.log("Proposal created with id:", id.toString()); + const agentTx = await factory.executeApplication(id, true); + await agentTx.wait(); + console.log(agentTx) + } +} + +(async () => { + try { + await createAgent(); + } catch (e) { + console.log(e); + } +})(); diff --git a/test/agentDAO.js b/test/agentDAO.js index fb95ab9..797d203 100644 --- a/test/agentDAO.js +++ b/test/agentDAO.js @@ -84,7 +84,7 @@ describe("AgentDAO", function () { ); await virtualToken.waitForDeployment(); - const AgentNft = await ethers.getContractFactory("AgentNft"); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); const contribution = await upgrades.deployProxy( diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index 81c11c6..f645e69 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -39,13 +39,13 @@ describe("AgentFactoryV2", function () { }; const getAccounts = async () => { - const [deployer, ipVault, founder, poorMan, trader] = + const [deployer, ipVault, founder, poorMan, trader, treasury] = await ethers.getSigners(); - return { deployer, ipVault, founder, poorMan, trader }; + return { deployer, ipVault, founder, poorMan, trader, treasury }; }; async function deployBaseContracts() { - const { deployer, ipVault } = await getAccounts(); + const { deployer, ipVault, treasury } = await getAccounts(); const virtualToken = await ethers.deployContract( "VirtualToken", @@ -54,7 +54,7 @@ describe("AgentFactoryV2", function () { ); await virtualToken.waitForDeployment(); - const AgentNft = await ethers.getContractFactory("AgentNft"); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); const contribution = await upgrades.deployProxy( @@ -107,6 +107,19 @@ describe("AgentFactoryV2", function () { await agentFactory.setMinter(minter.target); await agentFactory.setMaturityDuration(86400 * 365 * 10); // 100years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); + await agentFactory.setTokenAdmin(deployer.address); + await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION + ); + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address + ); + await agentFactory.setDefaultDelegatee(deployer.address); return { virtualToken, agentFactory, agentNft }; } @@ -147,7 +160,9 @@ describe("AgentFactoryV2", function () { const { agentFactory, applicationId } = base; const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId, false); + await agentFactory + .connect(founder) + .executeApplication(applicationId, false); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); @@ -170,7 +185,7 @@ describe("AgentFactoryV2", function () { before(async function () {}); - xit("should be able to propose a new agent", async function () { + it("should be able to propose a new agent", async function () { const { agentFactory, virtualToken } = await loadFixture( deployBaseContracts ); @@ -206,7 +221,7 @@ describe("AgentFactoryV2", function () { expect(id).to.be.equal(1n); }); - xit("should deny new Persona proposal when insufficient asset token", async function () { + it("should deny new Persona proposal when insufficient asset token", async function () { const { agentFactory } = await loadFixture(deployBaseContracts); const { poorMan } = await getAccounts(); await expect( @@ -225,13 +240,13 @@ describe("AgentFactoryV2", function () { ).to.be.revertedWith("Insufficient asset token"); }); - xit("should allow application execution by proposer", async function () { + it("should allow application execution by proposer", async function () { const { applicationId, agentFactory, virtualToken } = await loadFixture( deployWithApplication ); const { founder } = await getAccounts(); await expect( - agentFactory.connect(founder).executeApplication(applicationId) + agentFactory.connect(founder).executeApplication(applicationId, false) ).to.emit(agentFactory, "NewPersona"); // Check genesis components @@ -246,13 +261,13 @@ describe("AgentFactoryV2", function () { // C9: Stake liquidity token to get veToken }); - xit("agent component C1: Agent Token", async function () { + it("agent component C1: Agent Token", async function () { const { agent } = await loadFixture(deployWithAgent); const agentToken = await ethers.getContractAt("AgentToken", agent.token); expect(await agentToken.totalSupply()).to.be.equal(PROPOSAL_THRESHOLD); }); - xit("agent component C2: LP Pool", async function () { + it("agent component C2: LP Pool", async function () { const { agent, virtualToken } = await loadFixture(deployWithAgent); const lp = await ethers.getContractAt("IUniswapV2Pair", agent.lp); const t0 = await lp.token0(); @@ -267,7 +282,7 @@ describe("AgentFactoryV2", function () { expect(reserves[1]).to.be.equal(PROPOSAL_THRESHOLD); }); - xit("agent component C3: Agent veToken", async function () { + it("agent component C3: Agent veToken", async function () { const { agent } = await loadFixture(deployWithAgent); const { founder } = await getAccounts(); const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); @@ -277,14 +292,14 @@ describe("AgentFactoryV2", function () { ); }); - xit("agent component C4: Agent DAO", async function () { + it("agent component C4: Agent DAO", async function () { const { agent } = await loadFixture(deployWithAgent); const dao = await ethers.getContractAt("AgentDAO", agent.dao); expect(await dao.token()).to.be.equal(agent.veToken); expect(await dao.name()).to.be.equal(genesisInput.daoName); }); - xit("agent component C5: Agent NFT", async function () { + it("agent component C5: Agent NFT", async function () { const { agent, agentNft } = await loadFixture(deployWithAgent); const virtualInfo = await agentNft.virtualInfo(agent.virtualId); expect(virtualInfo.dao).to.equal(agent.dao); @@ -297,7 +312,7 @@ describe("AgentFactoryV2", function () { expect(virtualInfo.tba).to.equal(agent.tba); }); - xit("agent component C6: TBA", async function () { + it("agent component C6: TBA", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -323,7 +338,7 @@ describe("AgentFactoryV2", function () { expect(balance).to.be.equal(amount); }); - xit("agent component C7: Mint initial Agent tokens", async function () { + it("agent component C7: Mint initial Agent tokens", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -336,7 +351,7 @@ describe("AgentFactoryV2", function () { ); }); - xit("agent component C8: Provide liquidity", async function () { + it("agent component C8: Provide liquidity", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -370,10 +385,10 @@ describe("AgentFactoryV2", function () { ).to.be.at.most(10); expect( parseFloat(formatEther(await agentToken.balanceOf(trader.address))) - ).to.be.equal(90); + ).to.be.equal(87.3); // 90 - 3% tax }); - xit("agent component C9: Stake liquidity token to get veToken", async function () { + it("agent component C9: Stake liquidity token to get veToken", async function () { // TBA means whoever owns the NFT can move the account assets // We will test by minting VIRTUAL to the TBA and then use the treasury account to transfer it out const { agent, agentNft, virtualToken } = await loadFixture( @@ -418,8 +433,8 @@ describe("AgentFactoryV2", function () { .addLiquidity( agentToken.target, virtualToken.target, - amountToBuy, - capital, + parseEther("87.3"), + parseEther("90"), 0, 0, trader.address, @@ -428,11 +443,11 @@ describe("AgentFactoryV2", function () { expect( parseFloat(formatEther(await lpToken.balanceOf(trader.address))) - ).to.be.at.least(90); + ).to.be.at.least(80); expect(await agentToken.balanceOf(trader.address)).to.be.equal(0n); }); - xit("should allow staking on public agent", async function () { + it("should allow staking on public agent", async function () { // Need to provide LP first const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent @@ -478,8 +493,8 @@ describe("AgentFactoryV2", function () { .addLiquidity( agentToken.target, virtualToken.target, - amountToBuy, - capital, + parseEther("87.3"), + parseEther("90"), 0, 0, trader.address, @@ -493,7 +508,7 @@ describe("AgentFactoryV2", function () { .stake(parseEther("10"), trader.address, poorMan.address); }); - xit("should deny staking on private agent", async function () { + it("should deny staking on private agent", async function () { // Need to provide LP first const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent @@ -537,8 +552,8 @@ describe("AgentFactoryV2", function () { .addLiquidity( agentToken.target, virtualToken.target, - amountToBuy, - capital, + parseEther("87.3"), + parseEther("90"), 0, 0, trader.address, @@ -554,7 +569,7 @@ describe("AgentFactoryV2", function () { ).to.be.revertedWith("Staking is disabled for private agent"); }); - xit("should be able to set new validator and able to update score", async function () { + it("should be able to set new validator and able to update score", async function () { // Need to provide LP first const { agent, agentNft } = await loadFixture(deployWithAgent); const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); @@ -601,7 +616,7 @@ describe("AgentFactoryV2", function () { ).to.be.equal(1n); }); - xit("should be able to set new validator after created proposals and have correct score", async function () { + it("should be able to set new validator after created proposals and have correct score", async function () { const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent ); @@ -647,8 +662,8 @@ describe("AgentFactoryV2", function () { .addLiquidity( agentToken.target, virtualToken.target, - amountToBuy, - capital, + parseEther("87.3"), + parseEther("90"), 0, 0, trader.address, @@ -687,7 +702,7 @@ describe("AgentFactoryV2", function () { expect(newScore).to.be.equal(2n); }); - xit("should allow withdrawal", async function () { + it("should allow withdrawal", async function () { const { applicationId, agentFactory, virtualToken } = await loadFixture( deployWithApplication ); @@ -698,7 +713,7 @@ describe("AgentFactoryV2", function () { ); }); - xit("should lock initial LP", async function () { + it("should lock initial LP", async function () { const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent ); @@ -717,7 +732,7 @@ describe("AgentFactoryV2", function () { const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent ); - const {founder, deployer} = await getAccounts() + const { founder, deployer } = await getAccounts(); // Assign admin role await agentNft.grantRole(await agentNft.ADMIN_ROLE(), deployer); const agentVeToken = await ethers.getContractAt( @@ -728,8 +743,7 @@ describe("AgentFactoryV2", function () { expect(await agentNft.isBlacklisted(agent.virtualId)).to.be.equal(true); await expect(agentVeToken.setMatureAt(0)).to.not.be.reverted; - await expect( - agentVeToken.connect(founder).withdraw(parseEther("10")) - ).to.not.be.reverted; + await expect(agentVeToken.connect(founder).withdraw(parseEther("10"))).to + .not.be.reverted; }); }); From cef413dc2dda91b5f4bd1052653c65b6695c5cfa Mon Sep 17 00:00:00 2001 From: kw Date: Tue, 11 Jun 2024 15:26:49 +0800 Subject: [PATCH 19/30] done distribution logic --- contracts/AgentRewardV2.sol | 81 +++++++++++++++++++------------------ contracts/IAgentReward.sol | 18 +++------ 2 files changed, 47 insertions(+), 52 deletions(-) diff --git a/contracts/AgentRewardV2.sol b/contracts/AgentRewardV2.sol index f4bee88..e0da8d6 100644 --- a/contracts/AgentRewardV2.sol +++ b/contracts/AgentRewardV2.sol @@ -115,25 +115,26 @@ contract AgentRewardV2 is // Helper functions // ---------------- function getTotalStaked( - uint256[] memory virtualIds - ) internal view returns (uint256 totalStaked) { - uint length = virtualIds.length; - for (uint i = 0; i < length; i++) { - uint256 virtualId = virtualIds[i]; - uint256 staked = IERC20( - IAgentNft(agentNft).virtualLP(virtualId).veToken - ).totalSupply(); - totalStaked += staked; - } + uint256 virtualId + ) public view returns (uint256 totalStaked) { + return + IERC20(IAgentNft(agentNft).virtualLP(virtualId).veToken) + .totalSupply(); + } + + function getLPValue(uint256 virtualId) public view returns (uint256) { + address lp = IAgentNft(agentNft).virtualLP(virtualId).pool; + return IERC20(rewardToken).balanceOf(lp); } // ---------------- // Distribute rewards // ---------------- + // Distribute rewards to stakers and validators + // Reward source such as virtual specific revenue will share with protocol function distributeRewards( uint256 amount, - uint256 totalStaked, uint256[] memory virtualIds, bool shouldShareWithProtocol ) public onlyGov { @@ -154,21 +155,33 @@ contract AgentRewardV2 is uint256 balance = amount - protocolAmount; - uint32 rewardIndex = SafeCast.toUint32(_rewards.length - 1); + uint48 rewardIndex = SafeCast.toUint48(_rewards.length); - _rewards.push(Reward(block.number, balance, totalStaked, virtualIds)); + uint virtualCount = virtualIds.length; - emit NewReward(rewardIndex, balance, protocolAmount, totalStaked); + uint256[] memory lpValues = new uint256[](virtualCount); - // We don't expect too many virtuals here, the loop should not exceed gas limit - uint length = virtualIds.length; - for (uint i = 0; i < length; i++) { + uint256 totalLPValues = 0; + for (uint i = 0; i < virtualCount; i++) { + lpValues[i] = getLPValue(virtualIds[i]); + totalLPValues += lpValues[i]; + } + + if (totalLPValues <= 0) { + revert("Invalid LP values"); + } + + _rewards.push(Reward(block.number, balance, lpValues, virtualIds)); + + emit NewReward(rewardIndex, virtualIds); + + // We expect around 3-5 virtuals here, the loop should not exceed gas limit + for (uint i = 0; i < virtualCount; i++) { uint256 virtualId = virtualIds[i]; _distributeAgentReward( virtualId, rewardIndex, - balance, - totalStaked, + (lpValues[i] * balance) / totalLPValues, settings ); } @@ -177,41 +190,31 @@ contract AgentRewardV2 is function _distributeAgentReward( uint256 virtualId, uint48 rewardIndex, - uint256 totalAmount, - uint256 totalStaked, + uint256 amount, RewardSettingsCheckpoints.RewardSettings memory settings ) private { uint48 agentRewardId = _nextAgentRewardId++; - uint256 staked = IERC20( - IAgentNft(agentNft).virtualLP(virtualId).veToken - ).totalSupply(); - uint256 rewardAmount = (staked * totalAmount) / totalStaked; + uint256 totalStaked = getTotalStaked(virtualId); - uint256 stakerAmount = (rewardAmount * settings.stakerShares) / - DENOMINATOR; + uint256 stakerAmount = (amount * settings.stakerShares) / DENOMINATOR; - uint256 totalScore = IAgentDAO(IAgentNft(agentNft).virtualInfo(virtualId).dao) - .totalScore(); + uint256 totalProposals = IAgentDAO( + IAgentNft(agentNft).virtualInfo(virtualId).dao + ).proposalCount(); _agentRewards[virtualId].push( AgentReward( agentRewardId, rewardIndex, stakerAmount, - rewardAmount - stakerAmount, - totalScore + amount - stakerAmount, + totalProposals, + totalStaked ) ); - emit NewAgentReward( - virtualId, - agentRewardId, - rewardIndex, - stakerAmount, - rewardAmount - stakerAmount, - totalScore - ); + emit NewAgentReward(virtualId, agentRewardId); } function _distributeProtocolRewards( diff --git a/contracts/IAgentReward.sol b/contracts/IAgentReward.sol index 180cb64..745e5c1 100644 --- a/contracts/IAgentReward.sol +++ b/contracts/IAgentReward.sol @@ -5,7 +5,7 @@ interface IAgentReward { struct Reward { uint256 blockNumber; uint256 amount; - uint256 totalStaked; + uint256[] lpValues; uint256[] virtualIds; } @@ -15,7 +15,8 @@ interface IAgentReward { uint48 rewardIndex; uint256 stakerAmount; uint256 validatorAmount; - uint256 totalScore; + uint256 totalProposals; + uint256 totalStaked; } struct Claim { @@ -25,19 +26,10 @@ interface IAgentReward { event NewReward( uint48 pos, - uint256 amount, - uint256 protocolAmount, - uint256 totalStaked + uint256[] virtualIds ); - event NewAgentReward( - uint256 indexed virtualId, - uint48 id, - uint48 rewardIndex, - uint256 stakerAmount, - uint256 validatorAmount, - uint256 totalScore - ); + event NewAgentReward(uint256 indexed virtualId, uint48 id); /*struct ServiceReward { uint256 impact; From 6a9d8db4ecd6e544f1c66c579bf5abcfc7fde3a2 Mon Sep 17 00:00:00 2001 From: kw Date: Tue, 11 Jun 2024 17:01:30 +0800 Subject: [PATCH 20/30] done claim reward function --- contracts/AgentRewardV2.sol | 246 ++++++++++++++++++++++++++++++++++-- contracts/IAgentReward.sol | 73 ++--------- 2 files changed, 251 insertions(+), 68 deletions(-) diff --git a/contracts/AgentRewardV2.sol b/contracts/AgentRewardV2.sol index e0da8d6..11fe30c 100644 --- a/contracts/AgentRewardV2.sol +++ b/contracts/AgentRewardV2.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "./virtualPersona/IAgentNft.sol"; @@ -28,7 +29,7 @@ contract AgentRewardV2 is using SafeERC20 for IERC20; using RewardSettingsCheckpoints for RewardSettingsCheckpoints.Trace; - uint48 private _nextAgentRewardId; + uint256 private _nextAgentRewardId; uint256 public constant DENOMINATOR = 10000; bytes32 public constant GOV_ROLE = keccak256("GOV_ROLE"); @@ -92,13 +93,13 @@ contract AgentRewardV2 is return _rewardSettings.upperLookupRecent(timepoint); } - function getReward(uint48 pos) public view returns (Reward memory) { + function getReward(uint256 pos) public view returns (Reward memory) { return _rewards[pos]; } function getAgentReward( uint256 virtualId, - uint48 pos + uint256 pos ) public view returns (AgentReward memory) { return _agentRewards[virtualId][pos]; } @@ -154,11 +155,8 @@ contract AgentRewardV2 is : 0; uint256 balance = amount - protocolAmount; - - uint48 rewardIndex = SafeCast.toUint48(_rewards.length); - + uint256 rewardIndex = _rewards.length; uint virtualCount = virtualIds.length; - uint256[] memory lpValues = new uint256[](virtualCount); uint256 totalLPValues = 0; @@ -189,11 +187,11 @@ contract AgentRewardV2 is function _distributeAgentReward( uint256 virtualId, - uint48 rewardIndex, + uint256 rewardIndex, uint256 amount, RewardSettingsCheckpoints.RewardSettings memory settings ) private { - uint48 agentRewardId = _nextAgentRewardId++; + uint256 agentRewardId = _nextAgentRewardId++; uint256 totalStaked = getTotalStaked(virtualId); @@ -227,4 +225,234 @@ contract AgentRewardV2 is protocolRewards += protocolShares; return protocolShares; } + + // ---------------- + // Claim rewards + // ---------------- + mapping(address account => mapping(uint256 virtualId => Claim claim)) _stakerClaims; + mapping(address account => mapping(uint256 virtualId => Claim claim)) _validatorClaims; + + function getClaimableStakerRewards( + address account, + uint256 virtualId, + uint256 limit + ) public view returns (uint256 totalClaimable, uint256 numRewards) { + Claim memory claim = _stakerClaims[account][virtualId]; + numRewards = Math.min( + limit + claim.rewardCount, + getAgentRewardCount(virtualId) + ); + IAgentVeToken veToken = IAgentVeToken( + IAgentNft(agentNft).virtualLP(virtualId).veToken + ); + IAgentDAO dao = IAgentDAO( + IAgentNft(agentNft).virtualInfo(virtualId).dao + ); + for (uint i = claim.rewardCount; i < numRewards; i++) { + AgentReward memory agentReward = getAgentReward(virtualId, i); + Reward memory reward = getReward(agentReward.rewardIndex); + address delegatee = veToken.getPastDelegates( + account, + reward.blockNumber + ); + uint256 uptime = dao.getPastScore(delegatee, reward.blockNumber); + uint256 stakedAmount = veToken.getPastBalanceOf( + account, + reward.blockNumber + ); + uint256 stakerReward = (agentReward.stakerAmount * stakedAmount) / + agentReward.totalStaked; + stakerReward = + ((stakerReward * uptime * DENOMINATOR) / + agentReward.totalProposals) * + DENOMINATOR; + + totalClaimable += stakerReward; + } + } + + function getClaimableValidatorRewards( + address account, + uint256 virtualId, + uint256 limit + ) public view returns (uint256 totalClaimable, uint256 numRewards) { + Claim memory claim = _validatorClaims[account][virtualId]; + numRewards = Math.min( + limit + claim.rewardCount, + getAgentRewardCount(virtualId) + ); + IVotes veToken = IVotes( + IAgentNft(agentNft).virtualLP(virtualId).veToken + ); + IAgentDAO dao = IAgentDAO( + IAgentNft(agentNft).virtualInfo(virtualId).dao + ); + for (uint i = claim.rewardCount; i < numRewards; i++) { + AgentReward memory agentReward = getAgentReward(virtualId, i); + Reward memory reward = getReward(agentReward.rewardIndex); + uint256 uptime = dao.getPastScore(account, reward.blockNumber); + uint256 votes = veToken.getPastVotes(account, reward.blockNumber); + uint256 validatorReward = (agentReward.validatorAmount * votes) / + agentReward.totalStaked; + validatorReward = + ((validatorReward * uptime * DENOMINATOR) / + agentReward.totalProposals) * + DENOMINATOR; + + totalClaimable += validatorReward; + } + } + + function getTotalClaimableStakerRewards( + address account, + uint256[] memory virtualIds, + uint256 limit + ) public view returns (uint256 totalClaimable) { + for (uint i = 0; i < virtualIds.length; i++) { + uint256 virtualId = virtualIds[i]; + (uint256 claimable, ) = getClaimableStakerRewards(account, virtualId, limit); + totalClaimable += claimable; + } + } + + function getTotalClaimableValidatorRewards( + address account, + uint256[] memory virtualIds, + uint256 limit + ) public view returns (uint256 totalClaimable) { + for (uint i = 0; i < virtualIds.length; i++) { + uint256 virtualId = virtualIds[i]; + (uint256 claimable, ) = getClaimableValidatorRewards(account, virtualId, limit); + totalClaimable += claimable; + } + } + + function getAgentRewardCount( + uint256 virtualId + ) public view returns (uint256) { + return _agentRewards[virtualId].length; + } + + function claimStakerRewards( + uint256 virtualId, + uint256 limit + ) public noReentrant { + address account = _msgSender(); + uint256 totalClaimable; + uint256 numRewards; + (totalClaimable, numRewards) = getClaimableStakerRewards( + account, + virtualId, + limit + ); + + Claim storage claim = _stakerClaims[account][virtualId]; + claim.totalClaimed += totalClaimable; + claim.rewardCount = numRewards; + + IERC20(rewardToken).safeTransfer(account, totalClaimable); + + emit StakerRewardClaimed(virtualId, account, totalClaimable); + } + + function claimValidatorRewards( + uint256 virtualId, + uint256 limit + ) public noReentrant { + address account = _msgSender(); + uint256 totalClaimable; + uint256 numRewards; + (totalClaimable, numRewards) = getClaimableValidatorRewards( + account, + virtualId, + limit + ); + + Claim storage claim = _validatorClaims[account][virtualId]; + claim.totalClaimed += totalClaimable; + claim.rewardCount = numRewards; + + IERC20(rewardToken).safeTransfer(account, totalClaimable); + + emit ValidatorRewardClaimed(virtualId, account, totalClaimable); + } + + function claimAllStakerRewards( + uint256[] memory virtualIds, + uint256 limit + ) public noReentrant { + address account = _msgSender(); + uint256 totalClaimable; + for (uint i = 0; i < virtualIds.length; i++) { + uint256 virtualId = virtualIds[i]; + uint256 claimable; + uint256 numRewards; + (claimable, numRewards) = getClaimableStakerRewards( + account, + virtualId, + limit + ); + totalClaimable += claimable; + + Claim storage claim = _stakerClaims[account][virtualId]; + claim.totalClaimed += claimable; + claim.rewardCount = numRewards; + } + + IERC20(rewardToken).safeTransfer(account, totalClaimable); + } + + function claimAllValidatorRewards( + uint256[] memory virtualIds, + uint256 limit + ) public noReentrant { + address account = _msgSender(); + uint256 totalClaimable; + for (uint i = 0; i < virtualIds.length; i++) { + uint256 virtualId = virtualIds[i]; + uint256 claimable; + uint256 numRewards; + (claimable, numRewards) = getClaimableValidatorRewards( + account, + virtualId, + limit + ); + totalClaimable += claimable; + + Claim storage claim = _validatorClaims[account][virtualId]; + claim.totalClaimed += claimable; + claim.rewardCount = numRewards; + } + + IERC20(rewardToken).safeTransfer(account, totalClaimable); + } + + // ---------------- + // Manage parameters + // ---------------- + + function setRewardSettings( + uint16 protocolShares_, + uint16 stakerShares_ + ) public onlyGov { + _rewardSettings.push( + SafeCast.toUint32(block.number), + RewardSettingsCheckpoints.RewardSettings( + protocolShares_, + stakerShares_ + ) + ); + + emit RewardSettingsUpdated(protocolShares_, stakerShares_); + } + + function updateRefContracts( + address rewardToken_, + address agentNft_ + ) external onlyGov { + rewardToken = rewardToken_; + agentNft = agentNft_; + + emit RefContractsUpdated(rewardToken_, agentNft_); + } } diff --git a/contracts/IAgentReward.sol b/contracts/IAgentReward.sol index 745e5c1..a07fd71 100644 --- a/contracts/IAgentReward.sol +++ b/contracts/IAgentReward.sol @@ -11,8 +11,8 @@ interface IAgentReward { // Agent specific reward, the amount will be shared between stakers and validators struct AgentReward { - uint48 id; - uint48 rewardIndex; + uint256 id; + uint256 rewardIndex; uint256 stakerAmount; uint256 validatorAmount; uint256 totalProposals; @@ -21,74 +21,29 @@ interface IAgentReward { struct Claim { uint256 totalClaimed; - uint32 rewardCount; // Track number of reward blocks claimed to avoid reclaiming + uint256 rewardCount; // Track number of reward blocks claimed to avoid reclaiming } - event NewReward( - uint48 pos, - uint256[] virtualIds - ); + event NewReward(uint256 pos, uint256[] virtualIds); - event NewAgentReward(uint256 indexed virtualId, uint48 id); + event NewAgentReward(uint256 indexed virtualId, uint256 id); - /*struct ServiceReward { - uint256 impact; - uint256 amount; - uint256 parentAmount; - uint256 totalClaimed; - uint256 totalClaimedParent; - } + event RewardSettingsUpdated(uint16 protocolShares, uint16 stakerShares); - - - event RewardSettingsUpdated( - uint16 protocolShares, - uint16 contributorShares, - uint16 stakerShares, - uint16 parentShares, - uint256 stakeThreshold - ); - - event RefContractsUpdated( - address rewardToken, - address agentNft, - address contributionNft, - address serviceNft - ); - - event StakeThresholdUpdated(uint256 threshold); - - event ParentSharesUpdated(uint256 shares); + event RefContractsUpdated(address rewardToken, address agentNft); event StakerRewardClaimed( - uint256 virtualId, - uint256 amount, - address staker + uint256 indexed virtualId, + address indexed staker, + uint256 amount ); event ValidatorRewardClaimed( - uint256 virtualId, - uint256 amount, - address validator - ); - - event ServiceRewardsClaimed( - uint256 nftId, - address account, - uint256 total, - uint256 childrenAmount + uint256 indexed virtualId, + address indexed validator, + uint256 amount ); - - event NewAgentReward( - uint32 mainIndex, - uint256 virtualId, - uint256 validatorAmount, - uint256 contributorAmount, - uint256 coreAmount - ); - - event DatasetRewardsClaimed(uint256 nftId, address account, uint256 total); -*/ + error ERC5805FutureLookup(uint256 timepoint, uint32 clock); error NotGovError(); From 2e5ce7ba92d31a52a82a39ed169f3532a97a5dc7 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 12 Jun 2024 16:18:16 +0800 Subject: [PATCH 21/30] completed rewards test cases --- contracts/AgentRewardV2.sol | 63 +- contracts/IAgentReward.sol | 1 + contracts/token/Minter.sol | 24 +- contracts/virtualPersona/AgentVeToken.sol | 12 +- scripts/arguments/minter.js | 2 + test/agentDAO.js | 5 +- test/rewardsV2.js | 749 +++++++++++++++++----- test/virtualGenesis.js | 4 +- 8 files changed, 645 insertions(+), 215 deletions(-) diff --git a/contracts/AgentRewardV2.sol b/contracts/AgentRewardV2.sol index 11fe30c..9bfd1c5 100644 --- a/contracts/AgentRewardV2.sol +++ b/contracts/AgentRewardV2.sol @@ -33,6 +33,7 @@ contract AgentRewardV2 is uint256 public constant DENOMINATOR = 10000; bytes32 public constant GOV_ROLE = keccak256("GOV_ROLE"); + uint8 public constant LOOP_LIMIT = 100; // Referencing contracts address public rewardToken; @@ -234,12 +235,11 @@ contract AgentRewardV2 is function getClaimableStakerRewards( address account, - uint256 virtualId, - uint256 limit + uint256 virtualId ) public view returns (uint256 totalClaimable, uint256 numRewards) { Claim memory claim = _stakerClaims[account][virtualId]; numRewards = Math.min( - limit + claim.rewardCount, + LOOP_LIMIT + claim.rewardCount, getAgentRewardCount(virtualId) ); IAgentVeToken veToken = IAgentVeToken( @@ -262,10 +262,7 @@ contract AgentRewardV2 is ); uint256 stakerReward = (agentReward.stakerAmount * stakedAmount) / agentReward.totalStaked; - stakerReward = - ((stakerReward * uptime * DENOMINATOR) / - agentReward.totalProposals) * - DENOMINATOR; + stakerReward = (stakerReward * uptime) / agentReward.totalProposals; totalClaimable += stakerReward; } @@ -273,12 +270,11 @@ contract AgentRewardV2 is function getClaimableValidatorRewards( address account, - uint256 virtualId, - uint256 limit + uint256 virtualId ) public view returns (uint256 totalClaimable, uint256 numRewards) { Claim memory claim = _validatorClaims[account][virtualId]; numRewards = Math.min( - limit + claim.rewardCount, + LOOP_LIMIT + claim.rewardCount, getAgentRewardCount(virtualId) ); IVotes veToken = IVotes( @@ -295,9 +291,8 @@ contract AgentRewardV2 is uint256 validatorReward = (agentReward.validatorAmount * votes) / agentReward.totalStaked; validatorReward = - ((validatorReward * uptime * DENOMINATOR) / - agentReward.totalProposals) * - DENOMINATOR; + (validatorReward * uptime) / + agentReward.totalProposals; totalClaimable += validatorReward; } @@ -305,24 +300,28 @@ contract AgentRewardV2 is function getTotalClaimableStakerRewards( address account, - uint256[] memory virtualIds, - uint256 limit + uint256[] memory virtualIds ) public view returns (uint256 totalClaimable) { for (uint i = 0; i < virtualIds.length; i++) { uint256 virtualId = virtualIds[i]; - (uint256 claimable, ) = getClaimableStakerRewards(account, virtualId, limit); + (uint256 claimable, ) = getClaimableStakerRewards( + account, + virtualId + ); totalClaimable += claimable; } } function getTotalClaimableValidatorRewards( address account, - uint256[] memory virtualIds, - uint256 limit + uint256[] memory virtualIds ) public view returns (uint256 totalClaimable) { for (uint i = 0; i < virtualIds.length; i++) { uint256 virtualId = virtualIds[i]; - (uint256 claimable, ) = getClaimableValidatorRewards(account, virtualId, limit); + (uint256 claimable, ) = getClaimableValidatorRewards( + account, + virtualId + ); totalClaimable += claimable; } } @@ -334,16 +333,14 @@ contract AgentRewardV2 is } function claimStakerRewards( - uint256 virtualId, - uint256 limit + uint256 virtualId ) public noReentrant { address account = _msgSender(); uint256 totalClaimable; uint256 numRewards; (totalClaimable, numRewards) = getClaimableStakerRewards( account, - virtualId, - limit + virtualId ); Claim storage claim = _stakerClaims[account][virtualId]; @@ -352,20 +349,18 @@ contract AgentRewardV2 is IERC20(rewardToken).safeTransfer(account, totalClaimable); - emit StakerRewardClaimed(virtualId, account, totalClaimable); + emit StakerRewardClaimed(virtualId, account, numRewards, totalClaimable); } function claimValidatorRewards( - uint256 virtualId, - uint256 limit + uint256 virtualId ) public noReentrant { address account = _msgSender(); uint256 totalClaimable; uint256 numRewards; (totalClaimable, numRewards) = getClaimableValidatorRewards( account, - virtualId, - limit + virtualId ); Claim storage claim = _validatorClaims[account][virtualId]; @@ -378,8 +373,7 @@ contract AgentRewardV2 is } function claimAllStakerRewards( - uint256[] memory virtualIds, - uint256 limit + uint256[] memory virtualIds ) public noReentrant { address account = _msgSender(); uint256 totalClaimable; @@ -389,8 +383,7 @@ contract AgentRewardV2 is uint256 numRewards; (claimable, numRewards) = getClaimableStakerRewards( account, - virtualId, - limit + virtualId ); totalClaimable += claimable; @@ -403,8 +396,7 @@ contract AgentRewardV2 is } function claimAllValidatorRewards( - uint256[] memory virtualIds, - uint256 limit + uint256[] memory virtualIds ) public noReentrant { address account = _msgSender(); uint256 totalClaimable; @@ -414,8 +406,7 @@ contract AgentRewardV2 is uint256 numRewards; (claimable, numRewards) = getClaimableValidatorRewards( account, - virtualId, - limit + virtualId ); totalClaimable += claimable; diff --git a/contracts/IAgentReward.sol b/contracts/IAgentReward.sol index a07fd71..302c1f6 100644 --- a/contracts/IAgentReward.sol +++ b/contracts/IAgentReward.sol @@ -35,6 +35,7 @@ interface IAgentReward { event StakerRewardClaimed( uint256 indexed virtualId, address indexed staker, + uint256 numRewards, uint256 amount ); diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol index 61f9253..ccefa03 100644 --- a/contracts/token/Minter.sol +++ b/contracts/token/Minter.sol @@ -19,6 +19,9 @@ contract Minter is IMinter, Ownable { uint256 public ipShare; // Share for IP holder uint256 public dataShare; // Share for Dataset provider + uint256 public impactMultiplier; + + uint256 public constant DENOM = 10000; mapping(uint256 => bool) _mintedNfts; @@ -38,6 +41,8 @@ contract Minter is IMinter, Ownable { address contributionAddress, address agentAddress, uint256 _ipShare, + uint256 _dataShare, + uint256 _impactMultiplier, address _ipVault, address _agentFactory, address initialOwner @@ -46,6 +51,8 @@ contract Minter is IMinter, Ownable { contributionNft = contributionAddress; agentNft = agentAddress; ipShare = _ipShare; + dataShare = _dataShare; + impactMultiplier = _impactMultiplier; ipVault = _ipVault; agentFactory = _agentFactory; } @@ -79,6 +86,10 @@ contract Minter is IMinter, Ownable { agentFactory = _factory; } + function setImpactMultiplier(uint256 _multiplier) public onlyOwner { + impactMultiplier = _multiplier; + } + function mint(uint256 nftId) public noReentrant { // Mint configuration: // 1. ELO impact amount, to be shared between model and dataset owner @@ -95,15 +106,16 @@ contract Minter is IMinter, Ownable { _mintedNfts[nftId] = true; address tokenAddress = IAgentNft(agentNft).virtualInfo(agentId).token; - uint256 datasetId = IContributionNft(contributionNft).getDatasetId( - nftId - ); - uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * 10 ** 18); - uint256 ipAmount = (amount * ipShare) / 10000; + IContributionNft contribution = IContributionNft(contributionNft); + require(contribution.isModel(nftId), "Not a model contribution"); + + uint256 datasetId = contribution.getDatasetId(nftId); + uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * impactMultiplier * 10 ** 18) / DENOM; + uint256 ipAmount = (amount * ipShare) / DENOM; uint256 dataAmount = 0; if (datasetId != 0) { - dataAmount = (amount * ipShare) / 10000; + dataAmount = (amount * dataShare) / DENOM; amount = amount - dataAmount; } diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index 9cb556c..d2a4fd5 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -64,6 +64,14 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { address sender = _msgSender(); require(amount > 0, "Cannot stake 0"); + require( + IERC20(assetToken).balanceOf(sender) >= amount, + "Insufficient asset token balance" + ); + require( + IERC20(assetToken).allowance(sender, address(this)) >= amount, + "Insufficient asset token allowance" + ); if (totalSupply() == 0) { initialLock = amount; @@ -100,7 +108,9 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { address sender = _msgSender(); require(balanceOf(sender) >= amount, "Insufficient balance"); - if ((sender == founder) && ((balanceOf(sender) - amount) < initialLock)) { + if ( + (sender == founder) && ((balanceOf(sender) - amount) < initialLock) + ) { require(block.timestamp >= matureAt, "Not mature yet"); } diff --git a/scripts/arguments/minter.js b/scripts/arguments/minter.js index c0ed857..e621e4c 100644 --- a/scripts/arguments/minter.js +++ b/scripts/arguments/minter.js @@ -3,6 +3,8 @@ module.exports = [ process.env.CONTRIBUTION_NFT, process.env.VIRTUAL_NFT, process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, process.env.IP_VAULT, process.env.VIRTUAL_FACTORY, process.env.ADMIN, diff --git a/test/agentDAO.js b/test/agentDAO.js index 797d203..7a3aed4 100644 --- a/test/agentDAO.js +++ b/test/agentDAO.js @@ -32,7 +32,6 @@ function getDescHash(str) { describe("AgentDAO", function () { const PROPOSAL_THRESHOLD = parseEther("100000"); //100k const MATURITY_SCORE = toBeHex(2000, 32); // 20% - const IP_SHARE = 1000; // 10% const TOKEN_URI = "http://jessica"; @@ -128,7 +127,9 @@ describe("AgentDAO", function () { service.target, contribution.target, agentNft.target, - IP_SHARE, + process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, deployer.address, diff --git a/test/rewardsV2.js b/test/rewardsV2.js index 229aeec..38ce912 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -1,10 +1,3 @@ -/* -Test scenario: -1. Accounts: [validator1, staker1, validator2, staker2] -2. Stakes: [100000, 2000, 5000, 20000] -3. Uptime: [3,1] -4. All contribution NFTs are owned by account #10 -*/ const { expect } = require("chai"); const { toBeHex } = require("ethers/utils"); const abi = ethers.AbiCoder.defaultAbiCoder(); @@ -14,13 +7,6 @@ const { } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); const { parseEther, formatEther } = require("ethers"); -const getExecuteCallData = (factory, proposalId) => { - return factory.interface.encodeFunctionData("executeApplication", [ - proposalId, - false - ]); -}; - const getMintServiceCalldata = async (serviceNft, virtualId, hash) => { return serviceNft.interface.encodeFunctionData("mint", [virtualId, hash]); }; @@ -31,6 +17,7 @@ function getDescHash(str) { describe("RewardsV2", function () { const PROPOSAL_THRESHOLD = parseEther("100000"); //100k + const TREASURY_AMOUNT = parseEther("1000000"); //1M const MATURITY_SCORE = toBeHex(2000, 32); // 20% const IP_SHARE = 1000; // 10% @@ -59,6 +46,8 @@ describe("RewardsV2", function () { validator1, validator2, treasury, + virtualTreasury, + trader, ] = await ethers.getSigners(); return { deployer, @@ -69,20 +58,23 @@ describe("RewardsV2", function () { validator1, validator2, treasury, + virtualTreasury, + trader, }; }; async function deployBaseContracts() { - const { deployer, ipVault, treasury } = await getAccounts(); + const { deployer, ipVault, treasury, virtualTreasury } = + await getAccounts(); const virtualToken = await ethers.deployContract( "VirtualToken", - [PROPOSAL_THRESHOLD, deployer.address], + [TREASURY_AMOUNT, deployer.address], {} ); await virtualToken.waitForDeployment(); - const AgentNft = await ethers.getContractFactory("AgentNft"); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); const contribution = await upgrades.deployProxy( @@ -126,7 +118,9 @@ describe("RewardsV2", function () { service.target, contribution.target, agentNft.target, - IP_SHARE, + process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, deployer.address, @@ -147,6 +141,25 @@ describe("RewardsV2", function () { process.env.SWAP_THRESHOLD, treasury.address ); + await agentFactory.grantRole( + await agentFactory.WITHDRAW_ROLE(), + deployer.address + ); + + const rewards = await upgrades.deployProxy( + await ethers.getContractFactory("AgentRewardV2"), + [ + virtualToken.target, + agentNft.target, + { + protocolShares: process.env.PROTOCOL_SHARES, + stakerShares: process.env.STAKER_SHARES, + }, + ], + {} + ); + await rewards.waitForDeployment(); + await rewards.grantRole(await rewards.GOV_ROLE(), deployer.address); return { virtualToken, @@ -155,24 +168,22 @@ describe("RewardsV2", function () { serviceNft: service, contributionNft: contribution, minter, + rewards, }; } - async function deployWithApplication() { - const base = await deployBaseContracts(); + async function createApplication(base, founder, idx) { const { agentFactory, virtualToken } = base; - const { founder } = await getAccounts(); // Prepare tokens for proposal await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); await virtualToken .connect(founder) .approve(agentFactory.target, PROPOSAL_THRESHOLD); - const tx = await agentFactory .connect(founder) .proposeAgent( - genesisInput.name, + genesisInput.name + "-" + idx, genesisInput.symbol, genesisInput.tokenURI, genesisInput.cores, @@ -186,21 +197,40 @@ describe("RewardsV2", function () { const events = await agentFactory.queryFilter(filter, -1); const event = events[0]; const { id } = event.args; - return { applicationId: id, ...base }; + return id; } - async function deployWithAgent() { - const base = await deployWithApplication(); - const { agentFactory, applicationId } = base; + async function deployWithApplication() { + const base = await deployBaseContracts(); const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId, false); + const id = await createApplication(base, founder, 0); + return { applicationId: id, ...base }; + } + + async function createAgent(base, applicationId) { + const { agentFactory } = base; + await agentFactory.executeApplication(applicationId, true); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); const factoryEvent = factoryEvents[0]; + return factoryEvent.args; + } + + async function deployWithAgent() { + const base = await deployWithApplication(); + const { applicationId } = base; - const { virtualId, token, veToken, dao, tba, lp } = await factoryEvent.args; + const { founder } = await getAccounts(); + + const { virtualId, token, veToken, dao, tba, lp } = await createAgent( + base, + applicationId + ); + + const veTokenContract = await ethers.getContractAt("AgentVeToken", veToken); + await veTokenContract.connect(founder).delegate(founder.address); // We want to vote instead of letting default delegatee to vote return { ...base, @@ -215,61 +245,8 @@ describe("RewardsV2", function () { }; } - async function stakeAndVote() { - const signers = await ethers.getSigners(); - const [validator1, staker1, validator2, staker2] = signers; - const base = await deployGenesisVirtual(); - const Token = await ethers.getContractFactory("AgentToken"); - const token = Token.attach(base.persona.token); - const { persona, demoToken, personaNft, reward } = base; - // Staking - await personaNft.addValidator(1, validator2.address); - await demoToken.mint(staker1.address, STAKE_AMOUNTS[1]); - await demoToken.connect(staker1).approve(persona.token, STAKE_AMOUNTS[1]); - await token - .connect(staker1) - .stake(STAKE_AMOUNTS[1], staker1.address, validator1.address); - await demoToken.mint(validator2.address, STAKE_AMOUNTS[2]); - await demoToken - .connect(validator2) - .approve(persona.token, STAKE_AMOUNTS[2]); - await token - .connect(validator2) - .stake(STAKE_AMOUNTS[2], validator2.address, validator2.address); - await demoToken.mint(staker2.address, STAKE_AMOUNTS[3]); - await demoToken.connect(staker2).approve(persona.token, STAKE_AMOUNTS[3]); - await token - .connect(staker2) - .stake(STAKE_AMOUNTS[3], staker2.address, validator2.address); - - // Propose & validate - const Dao = await ethers.getContractFactory("AgentDAO"); - const dao = Dao.attach(persona.dao); - - const proposals = await Promise.all([ - dao - .propose([persona.token], [0], ["0x"], "Proposal 1") - .then((tx) => tx.wait()) - .then((receipt) => receipt.logs[0].args[0]), - dao - .propose([persona.token], [0], ["0x"], "Proposal 2") - .then((tx) => tx.wait()) - .then((receipt) => receipt.logs[0].args[0]), - ]); - await dao.castVote(proposals[0], 1); - await dao.connect(validator2).castVote(proposals[0], 1); - await dao.connect(validator2).castVote(proposals[1], 1); - - // Distribute rewards - await demoToken.mint(validator1, REWARD_AMOUNT); - await demoToken.approve(reward.target, REWARD_AMOUNT); - await reward.distributeRewards(REWARD_AMOUNT); - await reward.distributeRewardsForAgents(0, [1]); - - return { ...base }; - } - async function createContribution( + virtualId, coreId, maturity, parentId, @@ -277,17 +254,20 @@ describe("RewardsV2", function () { datasetId, desc, base, - account + account, + voters ) { - const { founder } = await getAccounts(); - const { agent, serviceNft, contributionNft, minter } = base; - const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + const { serviceNft, contributionNft, minter, agentNft } = base; + const daoAddr = (await agentNft.virtualInfo(virtualId)).dao; + const veAddr = (await agentNft.virtualLP(virtualId)).veToken; + const agentDAO = await ethers.getContractAt("AgentDAO", daoAddr); + const veToken = await ethers.getContractAt("AgentVeToken", veAddr); const descHash = getDescHash(desc); const mintCalldata = await getMintServiceCalldata( serviceNft, - agent.virtualId, + virtualId, descHash ); @@ -299,7 +279,7 @@ describe("RewardsV2", function () { await contributionNft.mint( account, - agent.virtualId, + virtualId, coreId, TOKEN_URI, proposalId, @@ -307,13 +287,16 @@ describe("RewardsV2", function () { isModel, datasetId ); - const voteParams = isModel ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) : "0x"; - await agentDAO - .connect(founder) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + + for (const voter of voters) { + await agentDAO + .connect(voter) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + } + await mine(600); await agentDAO.execute(proposalId); @@ -322,105 +305,389 @@ describe("RewardsV2", function () { return proposalId; } - async function prepareContributions() { - /* - NFT 1 (LLM DS) - NFT 2 (LLM Model) - NFT 3 (Voice DS) - NFT 4 (Voice Model *current) - NFT 5 (Visual model, no DS) - */ - const base = await stakeAndVote(); - const signers = await ethers.getSigners(); - const [validator1, staker1, validator2, staker2] = signers; - const contributionList = []; - const account = signers[10].address; - - // NFT 1 (LLM DS) - let nft = await createContribution( - 0, + before(async function () {}); + + it("should mint agent token for successful contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1, founder } = await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token + ); + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); + await createContribution( + 1, 0, + maturity, 0, - false, + true, 0, - "LLM DS", + "Test", base, - account + contributor1.address, + [founder] ); - contributionList.push(nft); + const balance2 = await agentToken.balanceOf(contributor1.address); + expect(balance2).to.equal(parseEther(maturity.toString())); + }); - // NFT 2 (LLM Model) - nft = await createContribution( + it("should mint agent token for IP owner on successful contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1 } = await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token + ); + const balance1 = await agentToken.balanceOf(ipVault.address); + expect(balance1).to.equal(0n); + await createContribution( + 1, 0, - 200, + maturity, 0, true, - nft, - "LLM Model", + 0, + "Test", base, - account + contributor1.address, + [founder] ); - contributionList.push(nft); - // NFT 3 (Voice DS) - nft = await createContribution( + const balance2 = await agentToken.balanceOf(ipVault.address); + expect(balance2).to.equal(parseEther((maturity * 0.1).toString())); + }); + + it("should be able to distribute protocol emission for single virtual", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent } = base; + const { contributor1, founder, validator1 } = await getAccounts(); + const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + await createContribution( 1, 0, + maturity, 0, - false, + true, 0, - "Voice DS", + "Test", base, - account + contributor1.address, + [validator1] ); - contributionList.push(nft); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await expect( + rewards.distributeRewards(parseEther(rewardSize.toString()), [1], false) + ).to.not.be.reverted; + }); - // NFT 4 (Voice Model *current) - nft = await createContribution( + it("should be able to claim correct amount for staker and validator (no protocol share)", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent } = base; + const { contributor1, founder, validator1 } = await getAccounts(); + const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + await createContribution( 1, - 100, + 0, + maturity, 0, true, - nft, - "Voice Model", + 0, + "Test", base, - account + contributor1.address, + [validator1] ); - contributionList.push(nft); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + false + ); + await mine(1); + // Founder has 90% of the total rewards + const founderSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const founderVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); + expect(founderSClaimable).to.equal("90000.0"); + expect(founderVClaimable).to.equal("0.0"); - nft = await createContribution( - 2, - 100, + // Validator has 10% of the total rewards + const validatorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) + ); + const validatorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards( + validator1.address, + [1], + 100 + ) + ); + expect(validatorSClaimable).to.equal("0.0"); + expect(validatorVClaimable).to.equal("10000.0"); + + // Nothing for contributor + const conributorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards( + contributor1.address, + [1], + 100 + ) + ); + const contributorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards( + contributor1.address, + [1], + 100 + ) + ); + expect(conributorSClaimable).to.equal("0.0"); + expect(contributorVClaimable).to.equal("0.0"); + }); + + it("should be able to claim correct amount for staker and validator (has protocol share)", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent } = base; + const { contributor1, founder, validator1 } = await getAccounts(); + const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + await createContribution( + 1, + 0, + maturity, 0, true, 0, - "Visual Model", + "Test", base, - account + contributor1.address, + [validator1] + ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + true ); - contributionList.push(nft); + await mine(1); + // Protocol shares = 10% = 10k + // Founder has 90% of the remaining rewards = 90% x 90k + const founderSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const founderVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); + expect(founderSClaimable).to.equal("81000.0"); + expect(founderVClaimable).to.equal("0.0"); - await base.demoToken.mint(validator1, REWARD_AMOUNT); - await base.demoToken.approve(base.reward.target, REWARD_AMOUNT); - await base.reward.distributeRewards(REWARD_AMOUNT); - await base.reward.distributeRewardsForAgents(1, [1]); + // Validator has 10% of the total rewards + const validatorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) + ); + const validatorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards( + validator1.address, + [1], + 100 + ) + ); + expect(validatorSClaimable).to.equal("0.0"); + expect(validatorVClaimable).to.equal("9000.0"); + + // Nothing for contributor + const conributorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards( + contributor1.address, + [1], + 100 + ) + ); + const contributorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards( + contributor1.address, + [1], + 100 + ) + ); + expect(conributorSClaimable).to.equal("0.0"); + expect(contributorVClaimable).to.equal("0.0"); + }); - return { contributionList, ...base }; - } + it("should be able to distribute protocol emission for multiple virtuals with arbitrary LP values", async function () { + const base = await loadFixture(deployWithAgent); + const { agent, agentNft, virtualToken, rewards } = base; + const { + contributor1, + contributor2, + validator1, + validator2, + founder, + trader, + } = await getAccounts(); + // Create 3 more virtuals to sum up to 4 + const app2 = await createApplication(base, founder, 1); + const agent2 = await createAgent(base, app2); + const app3 = await createApplication(base, founder, 2); + const agent3 = await createAgent(base, app3); + const app4 = await createApplication(base, founder, 3); + const agent4 = await createAgent(base, app4); + + // Create contributions for all 4 virtuals + for (let i = 1; i <= 4; i++) { + let veToken = await ethers.getContractAt( + "AgentVeToken", + ( + await agentNft.virtualLP(i) + ).veToken + ); - before(async function () {}); + await veToken.connect(founder).delegate(validator1.address); + await createContribution( + i, + 0, + 100, + 0, + true, + 0, + `Test ${i}`, + base, + contributor1.address, + [validator1] + ); + } - it("should mint agent token for successful contribution", async function () { + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + // Trade on different LP + await virtualToken.mint(trader.address, parseEther("300")); + await virtualToken + .connect(trader) + .approve(router.target, parseEther("300")); + for (let i of [1, 3, 4]) { + const agentTokenAddr = (await agentNft.virtualInfo(i)).token; + const amountToBuy = parseEther((20 * i).toString()); + const capital = parseEther("100"); + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agentTokenAddr], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000000) + ); + await mine(1); + } + + // Distribute rewards + // Expectations: + // virtual 4>3>1 + // virtual 2 = 0 + const rewardSize = 300000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1, 3, 4], + false + ); + await mine(1); + const rewards1 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [1] + ); + const rewards2 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [2] + ); + const rewards3 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [3] + ); + const rewards4 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [4] + ); + expect(rewards4).to.be.greaterThan(rewards3); + expect(rewards3).to.be.greaterThan(rewards1); + expect(rewards2).to.be.equal(0n); + }); + + it("should be able to distribute rewards based on validator uptime (validator2 is down)", async function () { const base = await loadFixture(deployWithAgent); - const { contributor1 } = await getAccounts(); - const maturity = 55; - const agentToken = await ethers.getContractAt( - "AgentToken", - base.agent.token + const { rewards, virtualToken, agent, agentNft } = base; + const { contributor1, founder, validator1, trader, validator2 } = + await getAccounts(); + const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER ); - const balance1 = await agentToken.balanceOf(contributor1.address); - expect(balance1).to.equal(0n); + + await virtualToken.mint(trader.address, parseEther("300")); + await virtualToken + .connect(trader) + .approve(router.target, parseEther("300")); + const agentTokenAddr = (await agentNft.virtualInfo(1)).token; + const amountToBuy = parseEther("100"); + const capital = parseEther("150"); + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agentTokenAddr], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000000) + ); + await mine(1); + await veToken.connect(trader).delegate(validator2.address); + await mine(1); + + // Validator 1 voting await createContribution( + 1, 0, maturity, 0, @@ -428,23 +695,120 @@ describe("RewardsV2", function () { 0, "Test", base, - contributor1.address + contributor1.address, + [validator1] ); - const balance2 = await agentToken.balanceOf(contributor1.address); - expect(balance2).to.equal(parseEther(maturity.toString())); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + false + ); + await mine(1); + + // Staker1 + Validator 1 + const staker1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const staker1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); + expect(staker1SClaimable).to.equal("90000.0"); + expect(staker1VClaimable).to.equal("0.0"); + + const validator1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) + ); + const validator1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) + ); + expect(validator1SClaimable).to.equal("0.0"); + expect(validator1VClaimable).to.equal("10000.0"); + + // Staker2 + Validator 2 + const staker2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(trader.address, [1]) + ); + const staker2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(trader.address, [1]) + ); + expect(staker2SClaimable).to.equal("0.0"); + expect(staker2VClaimable).to.equal("0.0"); + + const validator2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(trader.address, [1]) + ); + const validator2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(trader.address, [1]) + ); + expect(validator2SClaimable).to.equal("0.0"); + expect(validator2VClaimable).to.equal("0.0"); }); - it("should mint agent token for IP owner on successful contribution", async function () { + it("should be able to distribute rewards based on validator uptime (validator2 is up)", async function () { const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1 } = await getAccounts(); - const maturity = 55; - const agentToken = await ethers.getContractAt( - "AgentToken", - base.agent.token + const { rewards, virtualToken, agent, agentNft } = base; + const { contributor1, founder, validator1, trader, validator2 } = + await getAccounts(); + const maturity = 100; + + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const lp = await ethers.getContractAt("IERC20", agent.lp); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER ); - const balance1 = await agentToken.balanceOf(ipVault.address); - expect(balance1).to.equal(0n); + + await virtualToken.mint(trader.address, parseEther("100000")); + await virtualToken + .connect(trader) + .approve(router.target, parseEther("100000")); + const agentTokenAddr = (await agentNft.virtualInfo(1)).token; + const amountToBuy = parseEther("40000"); + const capital = parseEther("100000"); + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agentTokenAddr], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000000) + ); + await mine(1); + await agentToken + .connect(trader) + .approve(router.target, await agentToken.balanceOf(trader.address)); + await router + .connect(trader) + .addLiquidity( + agentToken.target, + virtualToken.target, + await agentToken.balanceOf(trader.address), + await virtualToken.balanceOf(trader.address), + 0, + 0, + trader.address, + Math.floor(new Date().getTime() / 1000 + 600000) + ); + await mine(1); + await lp.connect(trader).approve(veToken.target, await lp.balanceOf(trader.address)); + await veToken + .connect(trader) + .stake(await lp.balanceOf(trader.address), trader.address, validator2.address); + await mine(1); + + // Validator 1 voting await createContribution( + 1, 0, maturity, 0, @@ -452,10 +816,57 @@ describe("RewardsV2", function () { 0, "Test", base, - contributor1.address + contributor1.address, + [validator1, validator2] ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + false + ); + await mine(1); - const balance2 = await agentToken.balanceOf(ipVault.address); - expect(balance2).to.equal(parseEther((maturity * 0.1).toString())); + // Staker1 + Validator 1 + const staker1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const staker1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); + expect(parseFloat(staker1SClaimable)).to.be.greaterThan(70000); + expect(staker1VClaimable).to.equal("0.0"); + + const validator1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) + ); + const validator1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) + ); + expect(validator1SClaimable).to.equal("0.0"); + expect(parseFloat(validator1VClaimable)).to.be.greaterThan(7000); + + // Staker2 + Validator 2 + const staker2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(trader.address, [1]) + ); + const staker2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(trader.address, [1]) + ); + expect(parseFloat(staker2SClaimable)).to.be.greaterThan(10000); + expect(staker2VClaimable).to.equal("0.0"); + + const validator2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator2.address, [1]) + ); + const validator2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(validator2.address, [1]) + ); + expect(validator2SClaimable).to.equal("0.0"); + expect(parseFloat(validator2VClaimable)).to.be.greaterThan(1000); }); }); diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index 464dcfa..4e8e386 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -98,7 +98,9 @@ describe("AgentFactoryV2", function () { service.target, contribution.target, agentNft.target, - IP_SHARE, + process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, deployer.address, From b3b469f880e1de25700d23d27726c8104d5e3509 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 12 Jun 2024 16:20:47 +0800 Subject: [PATCH 22/30] fixed failed test cases --- test/rewardsV2.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/test/rewardsV2.js b/test/rewardsV2.js index 38ce912..32707a2 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -335,7 +335,7 @@ describe("RewardsV2", function () { it("should mint agent token for IP owner on successful contribution", async function () { const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1 } = await getAccounts(); + const { ipVault, contributor1, founder } = await getAccounts(); const maturity = 55; const agentToken = await ethers.getContractAt( "AgentToken", @@ -442,8 +442,7 @@ describe("RewardsV2", function () { const validatorVClaimable = formatEther( await rewards.getTotalClaimableValidatorRewards( validator1.address, - [1], - 100 + [1] ) ); expect(validatorSClaimable).to.equal("0.0"); @@ -453,15 +452,13 @@ describe("RewardsV2", function () { const conributorSClaimable = formatEther( await rewards.getTotalClaimableStakerRewards( contributor1.address, - [1], - 100 + [1] ) ); const contributorVClaimable = formatEther( await rewards.getTotalClaimableValidatorRewards( contributor1.address, - [1], - 100 + [1] ) ); expect(conributorSClaimable).to.equal("0.0"); @@ -519,8 +516,7 @@ describe("RewardsV2", function () { const validatorVClaimable = formatEther( await rewards.getTotalClaimableValidatorRewards( validator1.address, - [1], - 100 + [1] ) ); expect(validatorSClaimable).to.equal("0.0"); @@ -530,15 +526,13 @@ describe("RewardsV2", function () { const conributorSClaimable = formatEther( await rewards.getTotalClaimableStakerRewards( contributor1.address, - [1], - 100 + [1] ) ); const contributorVClaimable = formatEther( await rewards.getTotalClaimableValidatorRewards( contributor1.address, - [1], - 100 + [1] ) ); expect(conributorSClaimable).to.equal("0.0"); From 5ad64be13b9ab294c524b2329bc65c1369e6e751 Mon Sep 17 00:00:00 2001 From: kahwaipd <100838692+kahwaipd@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:22:44 +0800 Subject: [PATCH 23/30] Feat/reward v2 (#24) V2 upgrades --- .openzeppelin/base-sepolia.json | 1724 +++++++++++++++++- contracts/AgentRewardV2.sol | 433 ++++- contracts/IAgentReward.sol | 92 +- contracts/libs/RewardSettingsCheckpoints.sol | 7 +- contracts/token/Minter.sol | 24 +- contracts/virtualPersona/AgentDAO.sol | 7 + contracts/virtualPersona/AgentFactory.sol | 23 +- contracts/virtualPersona/AgentNftV2.sol | 1 - contracts/virtualPersona/AgentVeToken.sol | 38 +- contracts/virtualPersona/IAgentDAO.sol | 2 + contracts/virtualPersona/IAgentNft.sol | 4 + contracts/virtualPersona/IAgentVeToken.sol | 7 +- scripts/arguments/minter.js | 11 + scripts/v2/migrateV2.ts | 52 +- test/agentDAO.js | 8 +- test/contribution.js | 1 + test/delegate.js | 1 + test/rewardsV2.js | 742 ++++++-- test/virtualGenesis.js | 10 +- 19 files changed, 2875 insertions(+), 312 deletions(-) create mode 100644 scripts/arguments/minter.js diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json index e5fa923..e26f1bb 100644 --- a/.openzeppelin/base-sepolia.json +++ b/.openzeppelin/base-sepolia.json @@ -8269,6 +8269,1726 @@ }, "e10a30c86b83904166af84da2d83204bf5ffc9265d549b765929e1be23a49090": { "address": "0x426A2a2fcdA4851E411a5afDa7480263bd02E254", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "coreTypes", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_uint8,t_string_storage)", + "contract": "CoreRegistry", + "src": "contracts/virtualPersona/CoreRegistry.sol:7" + }, + { + "label": "_nextCoreType", + "offset": 0, + "slot": "1", + "type": "t_uint8", + "contract": "CoreRegistry", + "src": "contracts/virtualPersona/CoreRegistry.sol:8" + }, + { + "label": "_validatorsMap", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:8" + }, + { + "label": "_baseValidatorScore", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:10" + }, + { + "label": "_validators", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_array(t_address)dyn_storage)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:12" + }, + { + "label": "_getScoreOf", + "offset": 0, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:14" + }, + { + "label": "_getMaxScore", + "offset": 8, + "slot": "5", + "type": "t_function_internal_view(t_uint256)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:15" + }, + { + "label": "_getPastScore", + "offset": 16, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:16" + }, + { + "label": "_nextVirtualId", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:27" + }, + { + "label": "_stakingTokenToVirtualId", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:28" + }, + { + "label": "virtualInfos", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_struct(VirtualInfo)24559_storage)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:47" + }, + { + "label": "_contributionNft", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:49" + }, + { + "label": "_serviceNft", + "offset": 0, + "slot": "10", + "type": "t_address", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:50" + }, + { + "label": "_blacklists", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:54" + }, + { + "label": "virtualLPs", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_uint256,t_struct(VirtualLP)24571_storage)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:55" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)25_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_string_storage)": { + "label": "mapping(uint256 => string)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)25_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ERC721Storage)984_storage": { + "label": "struct ERC721Upgradeable.ERC721Storage", + "members": [ + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "2" + }, + { + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "offset": 0, + "slot": "3" + }, + { + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "4" + }, + { + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_struct(ERC721URIStorageStorage)1206_storage": { + "label": "struct ERC721URIStorageUpgradeable.ERC721URIStorageStorage", + "members": [ + { + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)744_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)25_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_function_internal_view(t_uint256)returns(t_uint256)": { + "label": "function (uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address)returns(t_uint256)": { + "label": "function (uint256,address) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)": { + "label": "function (uint256,address,uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_address)dyn_storage)": { + "label": "mapping(uint256 => address[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { + "label": "mapping(uint256 => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(VirtualInfo)24559_storage)": { + "label": "mapping(uint256 => struct IAgentNft.VirtualInfo)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(VirtualLP)24571_storage)": { + "label": "mapping(uint256 => struct IAgentNft.VirtualLP)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint8,t_string_storage)": { + "label": "mapping(uint8 => string)", + "numberOfBytes": "32" + }, + "t_struct(VirtualInfo)24559_storage": { + "label": "struct IAgentNft.VirtualInfo", + "members": [ + { + "label": "dao", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "founder", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "tba", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "coreTypes", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(VirtualLP)24571_storage": { + "label": "struct IAgentNft.VirtualLP", + "members": [ + { + "label": "pool", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "veToken", + "type": "t_address", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)25_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721URIStorage": [ + { + "contract": "ERC721URIStorageUpgradeable", + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol:25", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721": [ + { + "contract": "ERC721Upgradeable", + "label": "_name", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:27", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC721Upgradeable", + "label": "_symbol", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:30", + "offset": 0, + "slot": "1" + }, + { + "contract": "ERC721Upgradeable", + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:32", + "offset": 0, + "slot": "2" + }, + { + "contract": "ERC721Upgradeable", + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:34", + "offset": 0, + "slot": "3" + }, + { + "contract": "ERC721Upgradeable", + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:36", + "offset": 0, + "slot": "4" + }, + { + "contract": "ERC721Upgradeable", + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:38", + "offset": 0, + "slot": "5" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + }, + "allAddresses": [ + "0x426A2a2fcdA4851E411a5afDa7480263bd02E254", + "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3" + ] + }, + "7afde3df5370c0b1bba45fed6d45cdfcdc697c63d75ec2667077864a9d339f02": { + "address": "0x6B0A1335080B7860F3e8Da8c8aE98E4AadacB7e0", + "txHash": "0x859482b192575cfed90b27a12f967826d70b2fbad8d0207dd51f7958c1d3ecd9", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)7540_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:23" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:24" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:25" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:26" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:33" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)21854_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:70" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:72" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:83" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:85" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:97" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:98" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:99" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:100" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:107" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)744_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)21825": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)7540_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)21854_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)21854_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)21825", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)7540_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "298223e30fb076efde1b803399978fba144731385ba54bb99bda2f03ed3479a9": { + "address": "0x545247Deb3E0e9A7BE2ee80375484FB5d84d4E19", + "txHash": "0x580b50ce7a4f4e374027e8aa701c7b100ce8075cead142808b836a6002e83f95", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)275_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:23" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:24" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:25" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:26" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:33" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)2321_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:70" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:72" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:83" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:85" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:97" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:98" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:99" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:100" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:107" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)10_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)2292": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)275_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)2321_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)2321_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)2292", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)275_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "de19bf9b2a1efd3de8a1414f3ca6f29c35bebfb33463d1e2c12e2a38bb378550": { + "address": "0xE55bcbed5cF46aCAd261FD913B55880d509F3023", + "txHash": "0xfb9ca5e532d3c0ad4593b9980b50bc221959b47502544822b404a4e79b86c382", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)275_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:23" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:24" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:25" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:26" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:33" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)2321_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:70" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:72" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:83" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:85" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:97" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:98" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:99" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:100" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:107" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)10_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)2292": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)275_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)2321_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)2321_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)2292", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)275_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "671a77a28a29e9124728f2c5276e508b435755ba99494da3970c430865d7646f": { + "address": "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3", "layout": { "solcVersion": "0.8.20", "storage": [ @@ -8747,8 +10467,8 @@ } }, "allAddresses": [ - "0x426A2a2fcdA4851E411a5afDa7480263bd02E254", - "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3" + "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3", + "0x8f767259867Cc93df37e59ba445019418871ea57" ] } } diff --git a/contracts/AgentRewardV2.sol b/contracts/AgentRewardV2.sol index 4e0ab82..9bfd1c5 100644 --- a/contracts/AgentRewardV2.sol +++ b/contracts/AgentRewardV2.sol @@ -6,38 +6,54 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "./virtualPersona/IAgentNft.sol"; import "./virtualPersona/IAgentToken.sol"; +import "./virtualPersona/IAgentDAO.sol"; +import "./virtualPersona/IAgentVeToken.sol"; import "./libs/RewardSettingsCheckpoints.sol"; import "./contribution/IContributionNft.sol"; import "./contribution/IServiceNft.sol"; import "./libs/TokenSaver.sol"; import "./IAgentReward.sol"; -contract AgentRewardV2 { +contract AgentRewardV2 is + IAgentReward, + Initializable, + AccessControl, + TokenSaver +{ using Math for uint256; using SafeERC20 for IERC20; using RewardSettingsCheckpoints for RewardSettingsCheckpoints.Trace; - uint48 private _nextRewardId; + uint256 private _nextAgentRewardId; uint256 public constant DENOMINATOR = 10000; bytes32 public constant GOV_ROLE = keccak256("GOV_ROLE"); + uint8 public constant LOOP_LIMIT = 100; // Referencing contracts address public rewardToken; address public agentNft; - address public contributionNft; - address public serviceNft; - // modifier onlyGov() { - // if (!hasRole(GOV_ROLE, _msgSender())) { - // revert NotGovError(); - // } - // _; - // } + // Rewards checkpoints, split into Master reward and Virtual shares + Reward[] private _rewards; + mapping(uint256 virtualId => AgentReward[]) private _agentRewards; + + RewardSettingsCheckpoints.Trace private _rewardSettings; + + // Rewards ledger + uint256 public protocolRewards; + + modifier onlyGov() { + if (!hasRole(GOV_ROLE, _msgSender())) { + revert NotGovError(); + } + _; + } bool internal locked; @@ -48,19 +64,386 @@ contract AgentRewardV2 { locked = false; } - // function initialize( - // address rewardToken_, - // address agentNft_, - // address contributionNft_, - // address serviceNft_, - // RewardSettingsCheckpoints.RewardSettings memory settings_ - // ) external initializer { - // rewardToken = rewardToken_; - // agentNft = agentNft_; - // contributionNft = contributionNft_; - // serviceNft = serviceNft_; - // _rewardSettings.push(0, settings_); - // _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); - // _nextRewardId = 1; - // } + function initialize( + address rewardToken_, + address agentNft_, + RewardSettingsCheckpoints.RewardSettings memory settings_ + ) external initializer { + rewardToken = rewardToken_; + agentNft = agentNft_; + _rewardSettings.push(0, settings_); + _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + _nextAgentRewardId = 1; + } + + function getRewardSettings() + public + view + returns (RewardSettingsCheckpoints.RewardSettings memory) + { + return _rewardSettings.latest(); + } + + function getPastRewardSettings( + uint32 timepoint + ) public view returns (RewardSettingsCheckpoints.RewardSettings memory) { + uint32 currentTimepoint = SafeCast.toUint32(block.number); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } + return _rewardSettings.upperLookupRecent(timepoint); + } + + function getReward(uint256 pos) public view returns (Reward memory) { + return _rewards[pos]; + } + + function getAgentReward( + uint256 virtualId, + uint256 pos + ) public view returns (AgentReward memory) { + return _agentRewards[virtualId][pos]; + } + + function agentRewardCount(uint256 virtualId) public view returns (uint256) { + return _agentRewards[virtualId].length; + } + + function rewardCount() public view returns (uint256) { + return _rewards.length; + } + + // ---------------- + // Helper functions + // ---------------- + function getTotalStaked( + uint256 virtualId + ) public view returns (uint256 totalStaked) { + return + IERC20(IAgentNft(agentNft).virtualLP(virtualId).veToken) + .totalSupply(); + } + + function getLPValue(uint256 virtualId) public view returns (uint256) { + address lp = IAgentNft(agentNft).virtualLP(virtualId).pool; + return IERC20(rewardToken).balanceOf(lp); + } + + // ---------------- + // Distribute rewards + // ---------------- + + // Distribute rewards to stakers and validators + // Reward source such as virtual specific revenue will share with protocol + function distributeRewards( + uint256 amount, + uint256[] memory virtualIds, + bool shouldShareWithProtocol + ) public onlyGov { + require(amount > 0, "Invalid amount"); + + IERC20(rewardToken).safeTransferFrom( + _msgSender(), + address(this), + amount + ); + + RewardSettingsCheckpoints.RewardSettings + memory settings = getRewardSettings(); + + uint256 protocolAmount = shouldShareWithProtocol + ? _distributeProtocolRewards(amount) + : 0; + + uint256 balance = amount - protocolAmount; + uint256 rewardIndex = _rewards.length; + uint virtualCount = virtualIds.length; + uint256[] memory lpValues = new uint256[](virtualCount); + + uint256 totalLPValues = 0; + for (uint i = 0; i < virtualCount; i++) { + lpValues[i] = getLPValue(virtualIds[i]); + totalLPValues += lpValues[i]; + } + + if (totalLPValues <= 0) { + revert("Invalid LP values"); + } + + _rewards.push(Reward(block.number, balance, lpValues, virtualIds)); + + emit NewReward(rewardIndex, virtualIds); + + // We expect around 3-5 virtuals here, the loop should not exceed gas limit + for (uint i = 0; i < virtualCount; i++) { + uint256 virtualId = virtualIds[i]; + _distributeAgentReward( + virtualId, + rewardIndex, + (lpValues[i] * balance) / totalLPValues, + settings + ); + } + } + + function _distributeAgentReward( + uint256 virtualId, + uint256 rewardIndex, + uint256 amount, + RewardSettingsCheckpoints.RewardSettings memory settings + ) private { + uint256 agentRewardId = _nextAgentRewardId++; + + uint256 totalStaked = getTotalStaked(virtualId); + + uint256 stakerAmount = (amount * settings.stakerShares) / DENOMINATOR; + + uint256 totalProposals = IAgentDAO( + IAgentNft(agentNft).virtualInfo(virtualId).dao + ).proposalCount(); + + _agentRewards[virtualId].push( + AgentReward( + agentRewardId, + rewardIndex, + stakerAmount, + amount - stakerAmount, + totalProposals, + totalStaked + ) + ); + + emit NewAgentReward(virtualId, agentRewardId); + } + + function _distributeProtocolRewards( + uint256 amount + ) private returns (uint256) { + RewardSettingsCheckpoints.RewardSettings + memory rewardSettings = _rewardSettings.latest(); + uint256 protocolShares = (amount * rewardSettings.protocolShares) / + DENOMINATOR; + protocolRewards += protocolShares; + return protocolShares; + } + + // ---------------- + // Claim rewards + // ---------------- + mapping(address account => mapping(uint256 virtualId => Claim claim)) _stakerClaims; + mapping(address account => mapping(uint256 virtualId => Claim claim)) _validatorClaims; + + function getClaimableStakerRewards( + address account, + uint256 virtualId + ) public view returns (uint256 totalClaimable, uint256 numRewards) { + Claim memory claim = _stakerClaims[account][virtualId]; + numRewards = Math.min( + LOOP_LIMIT + claim.rewardCount, + getAgentRewardCount(virtualId) + ); + IAgentVeToken veToken = IAgentVeToken( + IAgentNft(agentNft).virtualLP(virtualId).veToken + ); + IAgentDAO dao = IAgentDAO( + IAgentNft(agentNft).virtualInfo(virtualId).dao + ); + for (uint i = claim.rewardCount; i < numRewards; i++) { + AgentReward memory agentReward = getAgentReward(virtualId, i); + Reward memory reward = getReward(agentReward.rewardIndex); + address delegatee = veToken.getPastDelegates( + account, + reward.blockNumber + ); + uint256 uptime = dao.getPastScore(delegatee, reward.blockNumber); + uint256 stakedAmount = veToken.getPastBalanceOf( + account, + reward.blockNumber + ); + uint256 stakerReward = (agentReward.stakerAmount * stakedAmount) / + agentReward.totalStaked; + stakerReward = (stakerReward * uptime) / agentReward.totalProposals; + + totalClaimable += stakerReward; + } + } + + function getClaimableValidatorRewards( + address account, + uint256 virtualId + ) public view returns (uint256 totalClaimable, uint256 numRewards) { + Claim memory claim = _validatorClaims[account][virtualId]; + numRewards = Math.min( + LOOP_LIMIT + claim.rewardCount, + getAgentRewardCount(virtualId) + ); + IVotes veToken = IVotes( + IAgentNft(agentNft).virtualLP(virtualId).veToken + ); + IAgentDAO dao = IAgentDAO( + IAgentNft(agentNft).virtualInfo(virtualId).dao + ); + for (uint i = claim.rewardCount; i < numRewards; i++) { + AgentReward memory agentReward = getAgentReward(virtualId, i); + Reward memory reward = getReward(agentReward.rewardIndex); + uint256 uptime = dao.getPastScore(account, reward.blockNumber); + uint256 votes = veToken.getPastVotes(account, reward.blockNumber); + uint256 validatorReward = (agentReward.validatorAmount * votes) / + agentReward.totalStaked; + validatorReward = + (validatorReward * uptime) / + agentReward.totalProposals; + + totalClaimable += validatorReward; + } + } + + function getTotalClaimableStakerRewards( + address account, + uint256[] memory virtualIds + ) public view returns (uint256 totalClaimable) { + for (uint i = 0; i < virtualIds.length; i++) { + uint256 virtualId = virtualIds[i]; + (uint256 claimable, ) = getClaimableStakerRewards( + account, + virtualId + ); + totalClaimable += claimable; + } + } + + function getTotalClaimableValidatorRewards( + address account, + uint256[] memory virtualIds + ) public view returns (uint256 totalClaimable) { + for (uint i = 0; i < virtualIds.length; i++) { + uint256 virtualId = virtualIds[i]; + (uint256 claimable, ) = getClaimableValidatorRewards( + account, + virtualId + ); + totalClaimable += claimable; + } + } + + function getAgentRewardCount( + uint256 virtualId + ) public view returns (uint256) { + return _agentRewards[virtualId].length; + } + + function claimStakerRewards( + uint256 virtualId + ) public noReentrant { + address account = _msgSender(); + uint256 totalClaimable; + uint256 numRewards; + (totalClaimable, numRewards) = getClaimableStakerRewards( + account, + virtualId + ); + + Claim storage claim = _stakerClaims[account][virtualId]; + claim.totalClaimed += totalClaimable; + claim.rewardCount = numRewards; + + IERC20(rewardToken).safeTransfer(account, totalClaimable); + + emit StakerRewardClaimed(virtualId, account, numRewards, totalClaimable); + } + + function claimValidatorRewards( + uint256 virtualId + ) public noReentrant { + address account = _msgSender(); + uint256 totalClaimable; + uint256 numRewards; + (totalClaimable, numRewards) = getClaimableValidatorRewards( + account, + virtualId + ); + + Claim storage claim = _validatorClaims[account][virtualId]; + claim.totalClaimed += totalClaimable; + claim.rewardCount = numRewards; + + IERC20(rewardToken).safeTransfer(account, totalClaimable); + + emit ValidatorRewardClaimed(virtualId, account, totalClaimable); + } + + function claimAllStakerRewards( + uint256[] memory virtualIds + ) public noReentrant { + address account = _msgSender(); + uint256 totalClaimable; + for (uint i = 0; i < virtualIds.length; i++) { + uint256 virtualId = virtualIds[i]; + uint256 claimable; + uint256 numRewards; + (claimable, numRewards) = getClaimableStakerRewards( + account, + virtualId + ); + totalClaimable += claimable; + + Claim storage claim = _stakerClaims[account][virtualId]; + claim.totalClaimed += claimable; + claim.rewardCount = numRewards; + } + + IERC20(rewardToken).safeTransfer(account, totalClaimable); + } + + function claimAllValidatorRewards( + uint256[] memory virtualIds + ) public noReentrant { + address account = _msgSender(); + uint256 totalClaimable; + for (uint i = 0; i < virtualIds.length; i++) { + uint256 virtualId = virtualIds[i]; + uint256 claimable; + uint256 numRewards; + (claimable, numRewards) = getClaimableValidatorRewards( + account, + virtualId + ); + totalClaimable += claimable; + + Claim storage claim = _validatorClaims[account][virtualId]; + claim.totalClaimed += claimable; + claim.rewardCount = numRewards; + } + + IERC20(rewardToken).safeTransfer(account, totalClaimable); + } + + // ---------------- + // Manage parameters + // ---------------- + + function setRewardSettings( + uint16 protocolShares_, + uint16 stakerShares_ + ) public onlyGov { + _rewardSettings.push( + SafeCast.toUint32(block.number), + RewardSettingsCheckpoints.RewardSettings( + protocolShares_, + stakerShares_ + ) + ); + + emit RewardSettingsUpdated(protocolShares_, stakerShares_); + } + + function updateRefContracts( + address rewardToken_, + address agentNft_ + ) external onlyGov { + rewardToken = rewardToken_; + agentNft = agentNft_; + + emit RefContractsUpdated(rewardToken_, agentNft_); + } } diff --git a/contracts/IAgentReward.sol b/contracts/IAgentReward.sol index e0c7e92..302c1f6 100644 --- a/contracts/IAgentReward.sol +++ b/contracts/IAgentReward.sol @@ -2,93 +2,49 @@ pragma solidity ^0.8.20; interface IAgentReward { - struct MainReward { - uint32 blockNumber; + struct Reward { + uint256 blockNumber; uint256 amount; - uint256 agentCount; - uint256 totalStaked; + uint256[] lpValues; + uint256[] virtualIds; } - // Virtual specific reward, the amount will be shared between validator pool and contributor pool - // Validator pool will be shared by validators and stakers - // Contributor pool will be shared by contribution NFT holders - struct Reward { - uint48 id; - uint32 mainIndex; - uint256 totalStaked; + // Agent specific reward, the amount will be shared between stakers and validators + struct AgentReward { + uint256 id; + uint256 rewardIndex; + uint256 stakerAmount; uint256 validatorAmount; - uint256 contributorAmount; - uint256 coreAmount; // Rewards per core + uint256 totalProposals; + uint256 totalStaked; } struct Claim { uint256 totalClaimed; - uint32 rewardCount; // Track number of reward blocks claimed to avoid reclaiming + uint256 rewardCount; // Track number of reward blocks claimed to avoid reclaiming } - struct ServiceReward { - uint256 impact; - uint256 amount; - uint256 parentAmount; - uint256 totalClaimed; - uint256 totalClaimedParent; - } + event NewReward(uint256 pos, uint256[] virtualIds); - event NewMainReward( - uint32 indexed pos, - uint256 amount, - uint256 agentCount, - uint256 totalStaked - ); + event NewAgentReward(uint256 indexed virtualId, uint256 id); - event RewardSettingsUpdated( - uint16 protocolShares, - uint16 contributorShares, - uint16 stakerShares, - uint16 parentShares, - uint256 stakeThreshold - ); + event RewardSettingsUpdated(uint16 protocolShares, uint16 stakerShares); - event RefContractsUpdated( - address rewardToken, - address agentNft, - address contributionNft, - address serviceNft - ); - - event StakeThresholdUpdated(uint256 threshold); - - event ParentSharesUpdated(uint256 shares); + event RefContractsUpdated(address rewardToken, address agentNft); event StakerRewardClaimed( - uint256 virtualId, - uint256 amount, - address staker + uint256 indexed virtualId, + address indexed staker, + uint256 numRewards, + uint256 amount ); event ValidatorRewardClaimed( - uint256 virtualId, - uint256 amount, - address validator - ); - - event ServiceRewardsClaimed( - uint256 nftId, - address account, - uint256 total, - uint256 childrenAmount + uint256 indexed virtualId, + address indexed validator, + uint256 amount ); - - event NewAgentReward( - uint32 mainIndex, - uint256 virtualId, - uint256 validatorAmount, - uint256 contributorAmount, - uint256 coreAmount - ); - - event DatasetRewardsClaimed(uint256 nftId, address account, uint256 total); - + error ERC5805FutureLookup(uint256 timepoint, uint32 clock); error NotGovError(); diff --git a/contracts/libs/RewardSettingsCheckpoints.sol b/contracts/libs/RewardSettingsCheckpoints.sol index 04bb8a4..ef9e19d 100644 --- a/contracts/libs/RewardSettingsCheckpoints.sol +++ b/contracts/libs/RewardSettingsCheckpoints.sol @@ -13,10 +13,7 @@ library RewardSettingsCheckpoints { struct RewardSettings { uint16 protocolShares; - uint16 contributorShares; uint16 stakerShares; - uint16 parentShares; // Optional rewards for contribution's parent - uint256 stakeThreshold; // Each VIRTUAL will require minimum amount of staked tokens to be considered for rewards } struct Checkpoint { @@ -62,7 +59,7 @@ library RewardSettingsCheckpoints { uint256 pos = self._checkpoints.length; return pos == 0 - ? RewardSettings(0, 0, 0, 0, 0) + ? RewardSettings(0, 0) : self._checkpoints[pos - 1]._value; } @@ -88,7 +85,7 @@ library RewardSettingsCheckpoints { return pos == 0 - ? RewardSettings(0, 0, 0, 0, 0) + ? RewardSettings(0, 0) : self._checkpoints[pos - 1]._value; } diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol index 61f9253..ccefa03 100644 --- a/contracts/token/Minter.sol +++ b/contracts/token/Minter.sol @@ -19,6 +19,9 @@ contract Minter is IMinter, Ownable { uint256 public ipShare; // Share for IP holder uint256 public dataShare; // Share for Dataset provider + uint256 public impactMultiplier; + + uint256 public constant DENOM = 10000; mapping(uint256 => bool) _mintedNfts; @@ -38,6 +41,8 @@ contract Minter is IMinter, Ownable { address contributionAddress, address agentAddress, uint256 _ipShare, + uint256 _dataShare, + uint256 _impactMultiplier, address _ipVault, address _agentFactory, address initialOwner @@ -46,6 +51,8 @@ contract Minter is IMinter, Ownable { contributionNft = contributionAddress; agentNft = agentAddress; ipShare = _ipShare; + dataShare = _dataShare; + impactMultiplier = _impactMultiplier; ipVault = _ipVault; agentFactory = _agentFactory; } @@ -79,6 +86,10 @@ contract Minter is IMinter, Ownable { agentFactory = _factory; } + function setImpactMultiplier(uint256 _multiplier) public onlyOwner { + impactMultiplier = _multiplier; + } + function mint(uint256 nftId) public noReentrant { // Mint configuration: // 1. ELO impact amount, to be shared between model and dataset owner @@ -95,15 +106,16 @@ contract Minter is IMinter, Ownable { _mintedNfts[nftId] = true; address tokenAddress = IAgentNft(agentNft).virtualInfo(agentId).token; - uint256 datasetId = IContributionNft(contributionNft).getDatasetId( - nftId - ); - uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * 10 ** 18); - uint256 ipAmount = (amount * ipShare) / 10000; + IContributionNft contribution = IContributionNft(contributionNft); + require(contribution.isModel(nftId), "Not a model contribution"); + + uint256 datasetId = contribution.getDatasetId(nftId); + uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * impactMultiplier * 10 ** 18) / DENOM; + uint256 ipAmount = (amount * ipShare) / DENOM; uint256 dataAmount = 0; if (datasetId != 0) { - dataAmount = (amount * ipShare) / 10000; + dataAmount = (amount * dataShare) / DENOM; amount = amount - dataAmount; } diff --git a/contracts/virtualPersona/AgentDAO.sol b/contracts/virtualPersona/AgentDAO.sol index 715b82c..90fa679 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -28,6 +28,8 @@ contract AgentDAO is error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + uint256 private _totalScore; + constructor() { _disableInitializers(); } @@ -164,6 +166,7 @@ contract AgentDAO is ); if (!votedPreviously && hasVoted(proposalId, account)) { + ++_totalScore; _scores[account].push( SafeCast.toUint48(block.number), SafeCast.toUint208(scoreOf(account)) + 1 @@ -238,4 +241,8 @@ contract AgentDAO is } return currentState; } + + function totalScore() public view override returns (uint256) { + return _totalScore; + } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index 4235268..cb91e34 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -94,11 +94,12 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { /////////////////////////////////////////////////////////////// // V2 Storage /////////////////////////////////////////////////////////////// - address[] public allTradingTokens; address private _uniswapRouter; - address public veTokenImplementation; address private _minter; address private _tokenAdmin; + + address[] public allTradingTokens; + address public veTokenImplementation; address public defaultDelegatee; // Default agent token params @@ -223,7 +224,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); } - function executeApplication(uint256 id) public noReentrant { + function executeApplication(uint256 id, bool canStake) public noReentrant { // This will bootstrap an Agent with following components: // C1: Agent Token // C2: LP Pool + Initial liquidity @@ -270,7 +271,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { string.concat("Staked ", application.name), string.concat("s", application.symbol), lp, - application.proposer + application.proposer, + canStake ); // C4 @@ -347,6 +349,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { string memory symbol, uint256 initialSupply ) internal returns (address instance) { + instance = Clones.clone(tokenImplementation); IAgentToken(instance).initialize( [_tokenAdmin, _uniswapRouter, assetToken], @@ -364,7 +367,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { string memory name, string memory symbol, address stakingAsset, - address founder + address founder, + bool canStake ) internal returns (address instance) { instance = Clones.clone(veTokenImplementation); IAgentVeToken(instance).initialize( @@ -373,7 +377,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { founder, stakingAsset, block.timestamp + maturityDuration, - address(nft) + address(nft), + canStake ); allTokens.push(instance); @@ -468,4 +473,10 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ) public onlyRole(DEFAULT_ADMIN_ROLE) { defaultDelegatee = newDelegatee; } + + function setAssetToken( + address newToken + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + assetToken = newToken; + } } diff --git a/contracts/virtualPersona/AgentNftV2.sol b/contracts/virtualPersona/AgentNftV2.sol index 570bce5..6ba9dd5 100644 --- a/contracts/virtualPersona/AgentNftV2.sol +++ b/contracts/virtualPersona/AgentNftV2.sol @@ -112,7 +112,6 @@ contract AgentNftV2 is _stakingTokenToVirtualId[address(daoToken)] = virtualId; _addValidator(virtualId, founder); - _initValidatorScore(virtualId, founder); return virtualId; } diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index 2f17ad7..d2a4fd5 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -19,7 +19,7 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { address public agentNft; uint256 public matureAt; // The timestamp when the founder can withdraw the tokens bool public canStake; // To control private/public agent mode - uint256 private initialLock; // Initial locked amount + uint256 public initialLock; // Initial locked amount constructor() { _disableInitializers(); @@ -37,20 +37,22 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { } function initialize( - string memory name, - string memory symbol, - address founder_, - address assetToken_, - uint256 matureAt_, - address agentNft_ + string memory _name, + string memory _symbol, + address _founder, + address _assetToken, + uint256 _matureAt, + address _agentNft, + bool _canStake ) external initializer { - __ERC20_init(name, symbol); + __ERC20_init(_name, _symbol); __ERC20Votes_init(); - founder = founder_; - matureAt = matureAt_; - assetToken = assetToken_; - agentNft = agentNft_; + founder = _founder; + matureAt = _matureAt; + assetToken = _assetToken; + agentNft = _agentNft; + canStake = _canStake; } // Stakers have to stake their tokens and delegate to a validator @@ -62,6 +64,14 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { address sender = _msgSender(); require(amount > 0, "Cannot stake 0"); + require( + IERC20(assetToken).balanceOf(sender) >= amount, + "Insufficient asset token balance" + ); + require( + IERC20(assetToken).allowance(sender, address(this)) >= amount, + "Insufficient asset token allowance" + ); if (totalSupply() == 0) { initialLock = amount; @@ -98,7 +108,9 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { address sender = _msgSender(); require(balanceOf(sender) >= amount, "Insufficient balance"); - if ((sender == founder) && ((balanceOf(sender) - amount) < initialLock)) { + if ( + (sender == founder) && ((balanceOf(sender) - amount) < initialLock) + ) { require(block.timestamp >= matureAt, "Not mature yet"); } diff --git a/contracts/virtualPersona/IAgentDAO.sol b/contracts/virtualPersona/IAgentDAO.sol index 4136eb0..5c3f098 100644 --- a/contracts/virtualPersona/IAgentDAO.sol +++ b/contracts/virtualPersona/IAgentDAO.sol @@ -17,6 +17,8 @@ interface IAgentDAO { function scoreOf(address account) external view returns (uint256); + function totalScore() external view returns (uint256); + function getPastScore( address account, uint256 timepoint diff --git a/contracts/virtualPersona/IAgentNft.sol b/contracts/virtualPersona/IAgentNft.sol index c73b602..a328465 100644 --- a/contracts/virtualPersona/IAgentNft.sol +++ b/contracts/virtualPersona/IAgentNft.sol @@ -40,6 +40,10 @@ interface IAgentNft is IValidatorRegistry { uint256 virtualId ) external view returns (VirtualInfo memory); + function virtualLP( + uint256 virtualId + ) external view returns (VirtualLP memory); + function totalSupply() external view returns (uint256); function totalStaked(uint256 virtualId) external view returns (uint256); diff --git a/contracts/virtualPersona/IAgentVeToken.sol b/contracts/virtualPersona/IAgentVeToken.sol index dba6f72..c03fd44 100644 --- a/contracts/virtualPersona/IAgentVeToken.sol +++ b/contracts/virtualPersona/IAgentVeToken.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.20; interface IAgentVeToken { function initialize( - string memory name, - string memory symbol, + string memory _name, + string memory _symbol, address _founder, address _assetToken, uint256 _matureAt, - address agentNft_ + address _agentNft, + bool _canStake ) external; function stake( diff --git a/scripts/arguments/minter.js b/scripts/arguments/minter.js new file mode 100644 index 0000000..e621e4c --- /dev/null +++ b/scripts/arguments/minter.js @@ -0,0 +1,11 @@ +module.exports = [ + process.env.SERVICE_NFT, + process.env.CONTRIBUTION_NFT, + process.env.VIRTUAL_NFT, + process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, + process.env.IP_VAULT, + process.env.VIRTUAL_FACTORY, + process.env.ADMIN, +]; diff --git a/scripts/v2/migrateV2.ts b/scripts/v2/migrateV2.ts index 05526ad..d3d51f9 100644 --- a/scripts/v2/migrateV2.ts +++ b/scripts/v2/migrateV2.ts @@ -5,17 +5,42 @@ const adminSigner = new ethers.Wallet( ethers.provider ); -async function upgradeFactory(implementations) { +const deployerSigner = new ethers.Wallet( + process.env.PRIVATE_KEY, + ethers.provider +); + + +async function upgradeFactory(implementations, assetToken) { const AgentFactory = await ethers.getContractFactory("AgentFactoryV2"); const factory = await upgrades.upgradeProxy( process.env.VIRTUAL_FACTORY, AgentFactory ); - await factory.setImplementations( - implementations.tokenImpl.target, - implementations.veTokenImpl.target, - implementations.daoImpl.target + if (implementations) { + await factory.setImplementations( + implementations.tokenImpl.target, + implementations.veTokenImpl.target, + implementations.daoImpl.target + ); + } + if (assetToken) { + await factory.setAssetToken(assetToken); + } + await factory.setTokenAdmin(process.env.ADMIN); + await factory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION + ); + await factory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + process.env.TAX_VAULT ); + await factory.setTokenMultiplier(10000); + await factory.setDefaultDelegatee(process.env.ADMIN); console.log("Upgraded FactoryV2", factory.target); } @@ -25,7 +50,7 @@ async function importContract() { adminSigner ); await upgrades.forceImport( - "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3", + "0x8f767259867Cc93df37e59ba445019418871ea57", ContractFactory ); } @@ -67,12 +92,21 @@ async function deployImplementations() { return { daoImpl, tokenImpl, veTokenImpl }; } +async function deployMinter() { + const deployParams = require("../arguments/minter") + const minter = await ethers.deployContract("Minter", deployParams); + await minter.waitForDeployment(); + console.log("Minter deployed to:", minter.target); +} + + (async () => { try { + // await deployMinter(); // const implementations = await deployImplementations(); - // await upgradeFactory(implementations); - //await deployAgentNft(); - await upgradeAgentNft(); + // await upgradeFactory(implementations, process.env.BRIDGED_TOKEN); + await deployAgentNft(); + //await upgradeAgentNft(); } catch (e) { console.log(e); } diff --git a/test/agentDAO.js b/test/agentDAO.js index ac28b7d..7a3aed4 100644 --- a/test/agentDAO.js +++ b/test/agentDAO.js @@ -17,6 +17,7 @@ const { parseEther, formatEther } = require("ethers"); const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, + false ]); }; @@ -31,7 +32,6 @@ function getDescHash(str) { describe("AgentDAO", function () { const PROPOSAL_THRESHOLD = parseEther("100000"); //100k const MATURITY_SCORE = toBeHex(2000, 32); // 20% - const IP_SHARE = 1000; // 10% const TOKEN_URI = "http://jessica"; @@ -127,7 +127,9 @@ describe("AgentDAO", function () { service.target, contribution.target, agentNft.target, - IP_SHARE, + process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, deployer.address, @@ -196,7 +198,7 @@ describe("AgentDAO", function () { const { agentFactory, applicationId } = base; const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId); + await agentFactory.connect(founder).executeApplication(applicationId, false); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); diff --git a/test/contribution.js b/test/contribution.js index a493a02..e4110aa 100644 --- a/test/contribution.js +++ b/test/contribution.js @@ -19,6 +19,7 @@ const { const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, + false ]); }; diff --git a/test/delegate.js b/test/delegate.js index 325b989..478de5f 100644 --- a/test/delegate.js +++ b/test/delegate.js @@ -11,6 +11,7 @@ const { const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, + false ]); }; diff --git a/test/rewardsV2.js b/test/rewardsV2.js index 70c016c..32707a2 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -1,10 +1,3 @@ -/* -Test scenario: -1. Accounts: [validator1, staker1, validator2, staker2] -2. Stakes: [100000, 2000, 5000, 20000] -3. Uptime: [3,1] -4. All contribution NFTs are owned by account #10 -*/ const { expect } = require("chai"); const { toBeHex } = require("ethers/utils"); const abi = ethers.AbiCoder.defaultAbiCoder(); @@ -14,12 +7,6 @@ const { } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); const { parseEther, formatEther } = require("ethers"); -const getExecuteCallData = (factory, proposalId) => { - return factory.interface.encodeFunctionData("executeApplication", [ - proposalId, - ]); -}; - const getMintServiceCalldata = async (serviceNft, virtualId, hash) => { return serviceNft.interface.encodeFunctionData("mint", [virtualId, hash]); }; @@ -30,6 +17,7 @@ function getDescHash(str) { describe("RewardsV2", function () { const PROPOSAL_THRESHOLD = parseEther("100000"); //100k + const TREASURY_AMOUNT = parseEther("1000000"); //1M const MATURITY_SCORE = toBeHex(2000, 32); // 20% const IP_SHARE = 1000; // 10% @@ -58,6 +46,8 @@ describe("RewardsV2", function () { validator1, validator2, treasury, + virtualTreasury, + trader, ] = await ethers.getSigners(); return { deployer, @@ -68,20 +58,23 @@ describe("RewardsV2", function () { validator1, validator2, treasury, + virtualTreasury, + trader, }; }; async function deployBaseContracts() { - const { deployer, ipVault, treasury } = await getAccounts(); + const { deployer, ipVault, treasury, virtualTreasury } = + await getAccounts(); const virtualToken = await ethers.deployContract( "VirtualToken", - [PROPOSAL_THRESHOLD, deployer.address], + [TREASURY_AMOUNT, deployer.address], {} ); await virtualToken.waitForDeployment(); - const AgentNft = await ethers.getContractFactory("AgentNft"); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); const contribution = await upgrades.deployProxy( @@ -125,7 +118,9 @@ describe("RewardsV2", function () { service.target, contribution.target, agentNft.target, - IP_SHARE, + process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, deployer.address, @@ -146,6 +141,25 @@ describe("RewardsV2", function () { process.env.SWAP_THRESHOLD, treasury.address ); + await agentFactory.grantRole( + await agentFactory.WITHDRAW_ROLE(), + deployer.address + ); + + const rewards = await upgrades.deployProxy( + await ethers.getContractFactory("AgentRewardV2"), + [ + virtualToken.target, + agentNft.target, + { + protocolShares: process.env.PROTOCOL_SHARES, + stakerShares: process.env.STAKER_SHARES, + }, + ], + {} + ); + await rewards.waitForDeployment(); + await rewards.grantRole(await rewards.GOV_ROLE(), deployer.address); return { virtualToken, @@ -154,24 +168,22 @@ describe("RewardsV2", function () { serviceNft: service, contributionNft: contribution, minter, + rewards, }; } - async function deployWithApplication() { - const base = await deployBaseContracts(); + async function createApplication(base, founder, idx) { const { agentFactory, virtualToken } = base; - const { founder } = await getAccounts(); // Prepare tokens for proposal await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); await virtualToken .connect(founder) .approve(agentFactory.target, PROPOSAL_THRESHOLD); - const tx = await agentFactory .connect(founder) .proposeAgent( - genesisInput.name, + genesisInput.name + "-" + idx, genesisInput.symbol, genesisInput.tokenURI, genesisInput.cores, @@ -185,21 +197,40 @@ describe("RewardsV2", function () { const events = await agentFactory.queryFilter(filter, -1); const event = events[0]; const { id } = event.args; - return { applicationId: id, ...base }; + return id; } - async function deployWithAgent() { - const base = await deployWithApplication(); - const { agentFactory, applicationId } = base; + async function deployWithApplication() { + const base = await deployBaseContracts(); const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId); + const id = await createApplication(base, founder, 0); + return { applicationId: id, ...base }; + } + + async function createAgent(base, applicationId) { + const { agentFactory } = base; + await agentFactory.executeApplication(applicationId, true); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); const factoryEvent = factoryEvents[0]; + return factoryEvent.args; + } + + async function deployWithAgent() { + const base = await deployWithApplication(); + const { applicationId } = base; - const { virtualId, token, veToken, dao, tba, lp } = await factoryEvent.args; + const { founder } = await getAccounts(); + + const { virtualId, token, veToken, dao, tba, lp } = await createAgent( + base, + applicationId + ); + + const veTokenContract = await ethers.getContractAt("AgentVeToken", veToken); + await veTokenContract.connect(founder).delegate(founder.address); // We want to vote instead of letting default delegatee to vote return { ...base, @@ -214,61 +245,8 @@ describe("RewardsV2", function () { }; } - async function stakeAndVote() { - const signers = await ethers.getSigners(); - const [validator1, staker1, validator2, staker2] = signers; - const base = await deployGenesisVirtual(); - const Token = await ethers.getContractFactory("AgentToken"); - const token = Token.attach(base.persona.token); - const { persona, demoToken, personaNft, reward } = base; - // Staking - await personaNft.addValidator(1, validator2.address); - await demoToken.mint(staker1.address, STAKE_AMOUNTS[1]); - await demoToken.connect(staker1).approve(persona.token, STAKE_AMOUNTS[1]); - await token - .connect(staker1) - .stake(STAKE_AMOUNTS[1], staker1.address, validator1.address); - await demoToken.mint(validator2.address, STAKE_AMOUNTS[2]); - await demoToken - .connect(validator2) - .approve(persona.token, STAKE_AMOUNTS[2]); - await token - .connect(validator2) - .stake(STAKE_AMOUNTS[2], validator2.address, validator2.address); - await demoToken.mint(staker2.address, STAKE_AMOUNTS[3]); - await demoToken.connect(staker2).approve(persona.token, STAKE_AMOUNTS[3]); - await token - .connect(staker2) - .stake(STAKE_AMOUNTS[3], staker2.address, validator2.address); - - // Propose & validate - const Dao = await ethers.getContractFactory("AgentDAO"); - const dao = Dao.attach(persona.dao); - - const proposals = await Promise.all([ - dao - .propose([persona.token], [0], ["0x"], "Proposal 1") - .then((tx) => tx.wait()) - .then((receipt) => receipt.logs[0].args[0]), - dao - .propose([persona.token], [0], ["0x"], "Proposal 2") - .then((tx) => tx.wait()) - .then((receipt) => receipt.logs[0].args[0]), - ]); - await dao.castVote(proposals[0], 1); - await dao.connect(validator2).castVote(proposals[0], 1); - await dao.connect(validator2).castVote(proposals[1], 1); - - // Distribute rewards - await demoToken.mint(validator1, REWARD_AMOUNT); - await demoToken.approve(reward.target, REWARD_AMOUNT); - await reward.distributeRewards(REWARD_AMOUNT); - await reward.distributeRewardsForAgents(0, [1]); - - return { ...base }; - } - async function createContribution( + virtualId, coreId, maturity, parentId, @@ -276,17 +254,20 @@ describe("RewardsV2", function () { datasetId, desc, base, - account + account, + voters ) { - const { founder } = await getAccounts(); - const { agent, serviceNft, contributionNft, minter } = base; - const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + const { serviceNft, contributionNft, minter, agentNft } = base; + const daoAddr = (await agentNft.virtualInfo(virtualId)).dao; + const veAddr = (await agentNft.virtualLP(virtualId)).veToken; + const agentDAO = await ethers.getContractAt("AgentDAO", daoAddr); + const veToken = await ethers.getContractAt("AgentVeToken", veAddr); const descHash = getDescHash(desc); const mintCalldata = await getMintServiceCalldata( serviceNft, - agent.virtualId, + virtualId, descHash ); @@ -298,7 +279,7 @@ describe("RewardsV2", function () { await contributionNft.mint( account, - agent.virtualId, + virtualId, coreId, TOKEN_URI, proposalId, @@ -306,13 +287,16 @@ describe("RewardsV2", function () { isModel, datasetId ); - const voteParams = isModel ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) : "0x"; - await agentDAO - .connect(founder) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + + for (const voter of voters) { + await agentDAO + .connect(voter) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + } + await mine(600); await agentDAO.execute(proposalId); @@ -321,105 +305,383 @@ describe("RewardsV2", function () { return proposalId; } - async function prepareContributions() { - /* - NFT 1 (LLM DS) - NFT 2 (LLM Model) - NFT 3 (Voice DS) - NFT 4 (Voice Model *current) - NFT 5 (Visual model, no DS) - */ - const base = await stakeAndVote(); - const signers = await ethers.getSigners(); - const [validator1, staker1, validator2, staker2] = signers; - const contributionList = []; - const account = signers[10].address; - - // NFT 1 (LLM DS) - let nft = await createContribution( - 0, + before(async function () {}); + + it("should mint agent token for successful contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1, founder } = await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token + ); + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); + await createContribution( + 1, 0, + maturity, 0, - false, + true, 0, - "LLM DS", + "Test", base, - account + contributor1.address, + [founder] ); - contributionList.push(nft); + const balance2 = await agentToken.balanceOf(contributor1.address); + expect(balance2).to.equal(parseEther(maturity.toString())); + }); - // NFT 2 (LLM Model) - nft = await createContribution( + it("should mint agent token for IP owner on successful contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1, founder } = await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token + ); + const balance1 = await agentToken.balanceOf(ipVault.address); + expect(balance1).to.equal(0n); + await createContribution( + 1, 0, - 200, + maturity, 0, true, - nft, - "LLM Model", + 0, + "Test", base, - account + contributor1.address, + [founder] ); - contributionList.push(nft); - // NFT 3 (Voice DS) - nft = await createContribution( + const balance2 = await agentToken.balanceOf(ipVault.address); + expect(balance2).to.equal(parseEther((maturity * 0.1).toString())); + }); + + it("should be able to distribute protocol emission for single virtual", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent } = base; + const { contributor1, founder, validator1 } = await getAccounts(); + const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + await createContribution( 1, 0, + maturity, 0, - false, + true, 0, - "Voice DS", + "Test", base, - account + contributor1.address, + [validator1] ); - contributionList.push(nft); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await expect( + rewards.distributeRewards(parseEther(rewardSize.toString()), [1], false) + ).to.not.be.reverted; + }); - // NFT 4 (Voice Model *current) - nft = await createContribution( + it("should be able to claim correct amount for staker and validator (no protocol share)", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent } = base; + const { contributor1, founder, validator1 } = await getAccounts(); + const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + await createContribution( 1, - 100, + 0, + maturity, 0, true, - nft, - "Voice Model", + 0, + "Test", base, - account + contributor1.address, + [validator1] ); - contributionList.push(nft); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + false + ); + await mine(1); + // Founder has 90% of the total rewards + const founderSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const founderVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); + expect(founderSClaimable).to.equal("90000.0"); + expect(founderVClaimable).to.equal("0.0"); - nft = await createContribution( - 2, - 100, + // Validator has 10% of the total rewards + const validatorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) + ); + const validatorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards( + validator1.address, + [1] + ) + ); + expect(validatorSClaimable).to.equal("0.0"); + expect(validatorVClaimable).to.equal("10000.0"); + + // Nothing for contributor + const conributorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards( + contributor1.address, + [1] + ) + ); + const contributorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards( + contributor1.address, + [1] + ) + ); + expect(conributorSClaimable).to.equal("0.0"); + expect(contributorVClaimable).to.equal("0.0"); + }); + + it("should be able to claim correct amount for staker and validator (has protocol share)", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent } = base; + const { contributor1, founder, validator1 } = await getAccounts(); + const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + await createContribution( + 1, + 0, + maturity, 0, true, 0, - "Visual Model", + "Test", base, - account + contributor1.address, + [validator1] + ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + true ); - contributionList.push(nft); + await mine(1); + // Protocol shares = 10% = 10k + // Founder has 90% of the remaining rewards = 90% x 90k + const founderSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const founderVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); + expect(founderSClaimable).to.equal("81000.0"); + expect(founderVClaimable).to.equal("0.0"); - await base.demoToken.mint(validator1, REWARD_AMOUNT); - await base.demoToken.approve(base.reward.target, REWARD_AMOUNT); - await base.reward.distributeRewards(REWARD_AMOUNT); - await base.reward.distributeRewardsForAgents(1, [1]); + // Validator has 10% of the total rewards + const validatorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) + ); + const validatorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards( + validator1.address, + [1] + ) + ); + expect(validatorSClaimable).to.equal("0.0"); + expect(validatorVClaimable).to.equal("9000.0"); + + // Nothing for contributor + const conributorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards( + contributor1.address, + [1] + ) + ); + const contributorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards( + contributor1.address, + [1] + ) + ); + expect(conributorSClaimable).to.equal("0.0"); + expect(contributorVClaimable).to.equal("0.0"); + }); - return { contributionList, ...base }; - } + it("should be able to distribute protocol emission for multiple virtuals with arbitrary LP values", async function () { + const base = await loadFixture(deployWithAgent); + const { agent, agentNft, virtualToken, rewards } = base; + const { + contributor1, + contributor2, + validator1, + validator2, + founder, + trader, + } = await getAccounts(); + // Create 3 more virtuals to sum up to 4 + const app2 = await createApplication(base, founder, 1); + const agent2 = await createAgent(base, app2); + const app3 = await createApplication(base, founder, 2); + const agent3 = await createAgent(base, app3); + const app4 = await createApplication(base, founder, 3); + const agent4 = await createAgent(base, app4); + + // Create contributions for all 4 virtuals + for (let i = 1; i <= 4; i++) { + let veToken = await ethers.getContractAt( + "AgentVeToken", + ( + await agentNft.virtualLP(i) + ).veToken + ); - before(async function () {}); + await veToken.connect(founder).delegate(validator1.address); + await createContribution( + i, + 0, + 100, + 0, + true, + 0, + `Test ${i}`, + base, + contributor1.address, + [validator1] + ); + } - it("should mint agent token for successful contribution", async function () { + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + // Trade on different LP + await virtualToken.mint(trader.address, parseEther("300")); + await virtualToken + .connect(trader) + .approve(router.target, parseEther("300")); + for (let i of [1, 3, 4]) { + const agentTokenAddr = (await agentNft.virtualInfo(i)).token; + const amountToBuy = parseEther((20 * i).toString()); + const capital = parseEther("100"); + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agentTokenAddr], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000000) + ); + await mine(1); + } + + // Distribute rewards + // Expectations: + // virtual 4>3>1 + // virtual 2 = 0 + const rewardSize = 300000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1, 3, 4], + false + ); + await mine(1); + const rewards1 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [1] + ); + const rewards2 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [2] + ); + const rewards3 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [3] + ); + const rewards4 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [4] + ); + expect(rewards4).to.be.greaterThan(rewards3); + expect(rewards3).to.be.greaterThan(rewards1); + expect(rewards2).to.be.equal(0n); + }); + + it("should be able to distribute rewards based on validator uptime (validator2 is down)", async function () { const base = await loadFixture(deployWithAgent); - const { contributor1 } = await getAccounts(); - const maturity = 55; - const agentToken = await ethers.getContractAt( - "AgentToken", - base.agent.token + const { rewards, virtualToken, agent, agentNft } = base; + const { contributor1, founder, validator1, trader, validator2 } = + await getAccounts(); + const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER ); - const balance1 = await agentToken.balanceOf(contributor1.address); - expect(balance1).to.equal(0n); + + await virtualToken.mint(trader.address, parseEther("300")); + await virtualToken + .connect(trader) + .approve(router.target, parseEther("300")); + const agentTokenAddr = (await agentNft.virtualInfo(1)).token; + const amountToBuy = parseEther("100"); + const capital = parseEther("150"); + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agentTokenAddr], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000000) + ); + await mine(1); + await veToken.connect(trader).delegate(validator2.address); + await mine(1); + + // Validator 1 voting await createContribution( + 1, 0, maturity, 0, @@ -427,23 +689,120 @@ describe("RewardsV2", function () { 0, "Test", base, - contributor1.address + contributor1.address, + [validator1] ); - const balance2 = await agentToken.balanceOf(contributor1.address); - expect(balance2).to.equal(parseEther(maturity.toString())); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + false + ); + await mine(1); + + // Staker1 + Validator 1 + const staker1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const staker1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); + expect(staker1SClaimable).to.equal("90000.0"); + expect(staker1VClaimable).to.equal("0.0"); + + const validator1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) + ); + const validator1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) + ); + expect(validator1SClaimable).to.equal("0.0"); + expect(validator1VClaimable).to.equal("10000.0"); + + // Staker2 + Validator 2 + const staker2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(trader.address, [1]) + ); + const staker2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(trader.address, [1]) + ); + expect(staker2SClaimable).to.equal("0.0"); + expect(staker2VClaimable).to.equal("0.0"); + + const validator2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(trader.address, [1]) + ); + const validator2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(trader.address, [1]) + ); + expect(validator2SClaimable).to.equal("0.0"); + expect(validator2VClaimable).to.equal("0.0"); }); - it("should mint agent token for IP owner on successful contribution", async function () { + it("should be able to distribute rewards based on validator uptime (validator2 is up)", async function () { const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1 } = await getAccounts(); - const maturity = 55; - const agentToken = await ethers.getContractAt( - "AgentToken", - base.agent.token + const { rewards, virtualToken, agent, agentNft } = base; + const { contributor1, founder, validator1, trader, validator2 } = + await getAccounts(); + const maturity = 100; + + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const lp = await ethers.getContractAt("IERC20", agent.lp); + await veToken.connect(founder).delegate(validator1.address); + await mine(1); + + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER ); - const balance1 = await agentToken.balanceOf(ipVault.address); - expect(balance1).to.equal(0n); + + await virtualToken.mint(trader.address, parseEther("100000")); + await virtualToken + .connect(trader) + .approve(router.target, parseEther("100000")); + const agentTokenAddr = (await agentNft.virtualInfo(1)).token; + const amountToBuy = parseEther("40000"); + const capital = parseEther("100000"); + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agentTokenAddr], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000000) + ); + await mine(1); + await agentToken + .connect(trader) + .approve(router.target, await agentToken.balanceOf(trader.address)); + await router + .connect(trader) + .addLiquidity( + agentToken.target, + virtualToken.target, + await agentToken.balanceOf(trader.address), + await virtualToken.balanceOf(trader.address), + 0, + 0, + trader.address, + Math.floor(new Date().getTime() / 1000 + 600000) + ); + await mine(1); + await lp.connect(trader).approve(veToken.target, await lp.balanceOf(trader.address)); + await veToken + .connect(trader) + .stake(await lp.balanceOf(trader.address), trader.address, validator2.address); + await mine(1); + + // Validator 1 voting await createContribution( + 1, 0, maturity, 0, @@ -451,10 +810,57 @@ describe("RewardsV2", function () { 0, "Test", base, - contributor1.address + contributor1.address, + [validator1, validator2] ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + false + ); + await mine(1); - const balance2 = await agentToken.balanceOf(ipVault.address); - expect(balance2).to.equal(parseEther((maturity * 0.1).toString())); + // Staker1 + Validator 1 + const staker1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const staker1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); + expect(parseFloat(staker1SClaimable)).to.be.greaterThan(70000); + expect(staker1VClaimable).to.equal("0.0"); + + const validator1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) + ); + const validator1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) + ); + expect(validator1SClaimable).to.equal("0.0"); + expect(parseFloat(validator1VClaimable)).to.be.greaterThan(7000); + + // Staker2 + Validator 2 + const staker2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(trader.address, [1]) + ); + const staker2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(trader.address, [1]) + ); + expect(parseFloat(staker2SClaimable)).to.be.greaterThan(10000); + expect(staker2VClaimable).to.equal("0.0"); + + const validator2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator2.address, [1]) + ); + const validator2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(validator2.address, [1]) + ); + expect(validator2SClaimable).to.equal("0.0"); + expect(parseFloat(validator2VClaimable)).to.be.greaterThan(1000); }); }); diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index 8369ce8..4e8e386 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -98,7 +98,9 @@ describe("AgentFactoryV2", function () { service.target, contribution.target, agentNft.target, - IP_SHARE, + process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, deployer.address, @@ -160,7 +162,9 @@ describe("AgentFactoryV2", function () { const { agentFactory, applicationId } = base; const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId); + await agentFactory + .connect(founder) + .executeApplication(applicationId, false); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); @@ -244,7 +248,7 @@ describe("AgentFactoryV2", function () { ); const { founder } = await getAccounts(); await expect( - agentFactory.connect(founder).executeApplication(applicationId) + agentFactory.connect(founder).executeApplication(applicationId, false) ).to.emit(agentFactory, "NewPersona"); // Check genesis components From 83f54eb5b8c180c918a6441d25dccd83bf5dc7c3 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 12 Jun 2024 17:55:04 +0800 Subject: [PATCH 24/30] added rewards deployment script --- contracts/AgentReward.sol.old | 645 ------------------------------- contracts/RewardTreasury.sol.old | 115 ------ scripts/arguments/rewardsV2.js | 8 + scripts/v2/deployRewards.ts | 20 + 4 files changed, 28 insertions(+), 760 deletions(-) delete mode 100644 contracts/AgentReward.sol.old delete mode 100644 contracts/RewardTreasury.sol.old create mode 100644 scripts/arguments/rewardsV2.js create mode 100644 scripts/v2/deployRewards.ts diff --git a/contracts/AgentReward.sol.old b/contracts/AgentReward.sol.old deleted file mode 100644 index dd3a7fc..0000000 --- a/contracts/AgentReward.sol.old +++ /dev/null @@ -1,645 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts/utils/math/Math.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/access/AccessControl.sol"; -import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import "./virtualPersona/IAgentNft.sol"; -import "./virtualPersona/IAgentToken.sol"; -import "./libs/RewardSettingsCheckpoints.sol"; -import "./contribution/IContributionNft.sol"; -import "./contribution/IServiceNft.sol"; -import "./libs/TokenSaver.sol"; -import "./IAgentReward.sol"; - -contract AgentReward is IAgentReward, Initializable, AccessControl, TokenSaver { - using Math for uint256; - using SafeERC20 for IERC20; - using RewardSettingsCheckpoints for RewardSettingsCheckpoints.Trace; - - uint48 private _nextRewardId; - - uint256 public constant DENOMINATOR = 10000; - bytes32 public constant GOV_ROLE = keccak256("GOV_ROLE"); - - // Referencing contracts - address public rewardToken; - address public agentNft; - address public contributionNft; - address public serviceNft; - - // Rewards checkpoints, split into Master reward and Virtual shares - MainReward[] private _mainRewards; - mapping(uint256 virtualId => Reward[]) private _rewards; - - RewardSettingsCheckpoints.Trace private _rewardSettings; - - // Rewards ledger - uint256 public protocolRewards; - uint256 public validatorPoolRewards; // Accumulate the penalties from missed proposal voting - mapping(address account => mapping(uint256 virtualId => Claim claim)) - private _claimedStakerRewards; - mapping(address account => mapping(uint256 virtualId => Claim claim)) - private _claimedValidatorRewards; - mapping(uint256 serviceId => ServiceReward) private _serviceRewards; - mapping(uint48 rewardId => mapping(uint8 coreType => uint256 impacts)) _rewardImpacts; - - mapping(address validator => mapping(uint48 rewardId => uint256 amount)) - private _validatorRewards; - - modifier onlyGov() { - if (!hasRole(GOV_ROLE, _msgSender())) { - revert NotGovError(); - } - _; - } - - bool internal locked; - - modifier noReentrant() { - require(!locked, "cannot reenter"); - locked = true; - _; - locked = false; - } - - function initialize( - address rewardToken_, - address agentNft_, - address contributionNft_, - address serviceNft_, - RewardSettingsCheckpoints.RewardSettings memory settings_ - ) external initializer { - rewardToken = rewardToken_; - agentNft = agentNft_; - contributionNft = contributionNft_; - serviceNft = serviceNft_; - _rewardSettings.push(0, settings_); - _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); - _nextRewardId = 1; - } - - function getRewardSettings() - public - view - returns (RewardSettingsCheckpoints.RewardSettings memory) - { - return _rewardSettings.latest(); - } - - function getPastRewardSettings( - uint32 timepoint - ) public view returns (RewardSettingsCheckpoints.RewardSettings memory) { - uint32 currentTimepoint = SafeCast.toUint32(block.number); - if (timepoint >= currentTimepoint) { - revert ERC5805FutureLookup(timepoint, currentTimepoint); - } - return _rewardSettings.upperLookupRecent(timepoint); - } - - function getMainReward(uint32 pos) public view returns (MainReward memory) { - return _mainRewards[pos]; - } - - function getReward( - uint256 virtualId, - uint32 pos - ) public view returns (Reward memory) { - return _rewards[virtualId][pos]; - } - - function rewardCount(uint256 virtualId) public view returns (uint256) { - return _rewards[virtualId].length; - } - - // ---------------- - // Distribute rewards - // ---------------- - - function distributeRewards(uint256 amount) public onlyGov returns (uint32) { - require(amount > 0, "Invalid amount"); - - IERC20(rewardToken).safeTransferFrom( - _msgSender(), - address(this), - amount - ); - - RewardSettingsCheckpoints.RewardSettings - memory settings = getRewardSettings(); - - uint256 protocolShares = _distributeProtocolRewards(amount); - - uint256 agentShares = amount - protocolShares; - uint256 agentCount = _prepareAgentsRewards(agentShares, settings); - - uint32 mainRewardIndex = SafeCast.toUint32(_mainRewards.length - 1); - - for (uint256 virtualId = 1; virtualId <= agentCount; virtualId++) { - _distributeAgentRewards(virtualId, mainRewardIndex, settings); - } - - return SafeCast.toUint32(_mainRewards.length - 1); - } - - function _distributeProtocolRewards( - uint256 amount - ) private returns (uint256) { - RewardSettingsCheckpoints.RewardSettings - memory rewardSettings = _rewardSettings.latest(); - uint256 protocolShares = (amount * rewardSettings.protocolShares) / - DENOMINATOR; - protocolRewards += protocolShares; - return protocolShares; - } - - // Prepare agent reward placeholders and calculate total staked tokens for all eligible agents - function _prepareAgentsRewards( - uint256 amount, - RewardSettingsCheckpoints.RewardSettings memory settings - ) private returns (uint256 agentCount) { - IAgentNft nft = IAgentNft(agentNft); - uint256 grandTotalStaked = 0; // Total staked amount for all personas - uint256 totalAgents = nft.totalSupply(); - uint32 mainPos = SafeCast.toUint32(_mainRewards.length); - - // Get staking amount for all agents - for (uint256 virtualId = 1; virtualId <= totalAgents; virtualId++) { - // Get staked amount - uint256 totalStaked = nft.totalStaked(virtualId); - if (totalStaked < settings.stakeThreshold) { - continue; - } - - agentCount++; - grandTotalStaked += totalStaked; - uint48 rewardId = _nextRewardId++; - - _rewards[virtualId].push( - Reward({ - id: rewardId, - mainIndex: mainPos, - totalStaked: totalStaked, - validatorAmount: 0, - contributorAmount: 0, - coreAmount: 0 - }) - ); - } - - _mainRewards.push( - MainReward( - SafeCast.toUint32(block.number), - amount, - agentCount, - grandTotalStaked - ) - ); - emit NewMainReward(mainPos, amount, agentCount, grandTotalStaked); - } - - // Calculate agent rewards based on staked weightage and distribute to all stakers, validators and contributors - function _distributeAgentRewards( - uint256 virtualId, - uint256 mainRewardIndex, - RewardSettingsCheckpoints.RewardSettings memory settings - ) private { - if (_rewards[virtualId].length == 0) { - return; - } - - MainReward memory mainReward = _mainRewards[mainRewardIndex]; - - Reward storage reward = _rewards[virtualId][ - _rewards[virtualId].length - 1 - ]; - if (reward.mainIndex != mainRewardIndex) { - return; - } - - // Calculate VIRTUAL reward based on staked weightage - uint256 amount = (mainReward.amount * reward.totalStaked) / - mainReward.totalStaked; - - reward.contributorAmount = - (amount * uint256(settings.contributorShares)) / - DENOMINATOR; - reward.validatorAmount = amount - reward.contributorAmount; - - _distributeValidatorRewards( - reward.validatorAmount, - virtualId, - reward.id, - reward.totalStaked - ); - _distributeContributorRewards( - reward.contributorAmount, - virtualId, - settings - ); - } - - // Calculate validator rewards based on votes weightage and participation rate - function _distributeValidatorRewards( - uint256 amount, - uint256 virtualId, - uint48 rewardId, - uint256 totalStaked - ) private { - IAgentNft nft = IAgentNft(agentNft); - // Calculate weighted validator shares - uint256 validatorCount = nft.validatorCount(virtualId); - uint256 totalProposals = nft.totalProposals(virtualId); - - for (uint256 i = 0; i < validatorCount; i++) { - address validator = nft.validatorAt(virtualId, i); - - // Get validator revenue by votes weightage - address stakingAddress = nft.virtualInfo(virtualId).token; - uint256 votes = IERC5805(stakingAddress).getVotes(validator); - uint256 validatorRewards = (amount * votes) / totalStaked; - - // Calc validator reward based on participation rate - uint256 participationReward = totalProposals == 0 - ? 0 - : (validatorRewards * - nft.validatorScore(virtualId, validator)) / totalProposals; - _validatorRewards[validator][rewardId] = participationReward; - - validatorPoolRewards += validatorRewards - participationReward; - } - } - - function _distributeContributorRewards( - uint256 amount, - uint256 virtualId, - RewardSettingsCheckpoints.RewardSettings memory settings - ) private { - IAgentNft nft = IAgentNft(agentNft); - uint8[] memory coreTypes = nft.virtualInfo(virtualId).coreTypes; - IServiceNft serviceNftContract = IServiceNft(serviceNft); - IContributionNft contributionNftContract = IContributionNft( - contributionNft - ); - - Reward storage reward = _rewards[virtualId][ - _rewards[virtualId].length - 1 - ]; - reward.coreAmount = amount / coreTypes.length; - uint256[] memory services = nft.getAllServices(virtualId); - - // Populate service impacts - uint256 serviceId; - uint256 impact; - for (uint i = 0; i < services.length; i++) { - serviceId = services[i]; - impact = serviceNftContract.getImpact(serviceId); - if (impact == 0) { - continue; - } - - ServiceReward storage serviceReward = _serviceRewards[serviceId]; - if (serviceReward.impact == 0) { - serviceReward.impact = impact; - } - _rewardImpacts[reward.id][ - serviceNftContract.getCore(serviceId) - ] += impact; - } - - // Distribute service rewards - uint256 impactAmount = 0; - uint256 parentAmount = 0; - uint256 parentShares = uint256(settings.parentShares); - for (uint i = 0; i < services.length; i++) { - serviceId = services[i]; - ServiceReward storage serviceReward = _serviceRewards[serviceId]; - if (serviceReward.impact == 0) { - continue; - } - impactAmount = - (reward.coreAmount * serviceReward.impact) / - _rewardImpacts[reward.id][ - serviceNftContract.getCore(serviceId) - ]; - parentAmount = contributionNftContract.getParentId(serviceId) == 0 - ? 0 - : ((impactAmount * parentShares) / DENOMINATOR); - - serviceReward.amount += impactAmount - parentAmount; - serviceReward.parentAmount += parentAmount; - } - } - - // ---------------- - // Functions to query rewards - // ---------------- - function _getClaimableStakerRewardsAt( - uint256 pos, - uint256 virtualId, - address account, - address stakingAddress - ) private view returns (uint256) { - Reward memory reward = getReward(virtualId, SafeCast.toUint32(pos)); - MainReward memory mainReward = getMainReward(reward.mainIndex); - IAgentToken token = IAgentToken(stakingAddress); - - address delegatee = token.getPastDelegates( - account, - mainReward.blockNumber - ); - - if (delegatee == address(0)) { - return 0; - } - - RewardSettingsCheckpoints.RewardSettings - memory settings = getPastRewardSettings(mainReward.blockNumber); - - uint256 validatorGroupRewards = _validatorRewards[delegatee][reward.id]; - - uint256 tokens = token.getPastBalanceOf( - account, - mainReward.blockNumber - ); - uint256 votes = IERC5805(stakingAddress).getPastVotes( - delegatee, - mainReward.blockNumber - ); - - return - (((validatorGroupRewards * tokens) / votes) * - uint256(settings.stakerShares)) / DENOMINATOR; - } - - function _getClaimableStakerRewards( - address staker, - uint256 virtualId - ) internal view returns (uint256) { - uint256 count = rewardCount(virtualId); - if (count == 0) { - return 0; - } - - address stakingAddress = IAgentNft(agentNft) - .virtualInfo(virtualId) - .token; - - Claim memory claim = _claimedStakerRewards[staker][virtualId]; - uint256 total = 0; - for (uint256 i = claim.rewardCount; i < count; i++) { - total += _getClaimableStakerRewardsAt( - i, - virtualId, - staker, - stakingAddress - ); - } - - return total; - } - - function _getClaimableValidatorRewardsAt( - uint256 pos, - uint256 virtualId, - address validator - ) internal view returns (uint256) { - Reward memory reward = getReward(virtualId, SafeCast.toUint32(pos)); - MainReward memory mainReward = getMainReward(reward.mainIndex); - RewardSettingsCheckpoints.RewardSettings - memory rewardSettings = getPastRewardSettings( - mainReward.blockNumber - ); - - uint256 validatorGroupRewards = _validatorRewards[validator][reward.id]; - - return - (validatorGroupRewards * - (DENOMINATOR - uint256(rewardSettings.stakerShares))) / - DENOMINATOR; - } - - function _getClaimableValidatorRewards( - address validator, - uint256 virtualId - ) internal view returns (uint256) { - uint256 count = rewardCount(virtualId); - if (count == 0) { - return 0; - } - - Claim memory claim = _claimedValidatorRewards[validator][virtualId]; - uint256 total = 0; - for (uint256 i = claim.rewardCount; i < count; i++) { - total += _getClaimableValidatorRewardsAt(i, virtualId, validator); - } - - return total; - } - - function getChildrenRewards(uint256 nftId) public view returns (uint256) { - uint256 childrenAmount = 0; - uint256[] memory children = IContributionNft(contributionNft) - .getChildren(nftId); - - ServiceReward memory childReward; - for (uint256 i = 0; i < children.length; i++) { - childReward = getServiceReward(children[i]); - childrenAmount += (childReward.parentAmount - - childReward.totalClaimedParent); - } - return childrenAmount; - } - - function _getClaimableServiceRewards( - uint256 nftId - ) internal view returns (uint256 total) { - ServiceReward memory serviceReward = getServiceReward(nftId); - total = serviceReward.amount - serviceReward.totalClaimed; - uint256 childrenAmount = getChildrenRewards(nftId); - total += childrenAmount; - } - - // ---------------- - // Functions to claim rewards - // ---------------- - function _claimStakerRewards( - address account, - uint256 virtualId - ) internal noReentrant { - uint256 amount = _getClaimableStakerRewards(account, virtualId); - if (amount == 0) { - return; - } - - uint256 count = rewardCount(virtualId); - - Claim storage claim = _claimedStakerRewards[account][virtualId]; - claim.rewardCount = SafeCast.toUint32(count); - claim.totalClaimed += amount; - emit StakerRewardClaimed(virtualId, amount, account); - - IERC20(rewardToken).safeTransfer(account, amount); - } - - function _claimValidatorRewards(uint256 virtualId) internal noReentrant { - address account = _msgSender(); - - uint256 amount = _getClaimableValidatorRewards(account, virtualId); - if (amount == 0) { - return; - } - - uint256 count = rewardCount(virtualId); - - Claim storage claim = _claimedValidatorRewards[account][virtualId]; - claim.rewardCount = SafeCast.toUint32(count); - claim.totalClaimed += amount; - emit ValidatorRewardClaimed(virtualId, amount, account); - - IERC20(rewardToken).safeTransfer(account, amount); - } - - function withdrawProtocolRewards(address recipient) external onlyGov { - require(protocolRewards > 0, "No protocol rewards"); - IERC20(rewardToken).safeTransfer(recipient, protocolRewards); - protocolRewards = 0; - } - - function withdrawValidatorPoolRewards(address recipient) external onlyGov { - require(validatorPoolRewards > 0, "No validator pool rewards"); - IERC20(rewardToken).safeTransfer(recipient, validatorPoolRewards); - validatorPoolRewards = 0; - } - - function getServiceReward( - uint256 nftId - ) public view returns (ServiceReward memory) { - return _serviceRewards[nftId]; - } - - function _claimServiceRewards(uint256 nftId) internal { - address account = _msgSender(); - require( - IERC721(contributionNft).ownerOf(nftId) == account, - "Not NFT owner" - ); - - ServiceReward storage serviceReward = _serviceRewards[nftId]; - uint256 total = (serviceReward.amount - serviceReward.totalClaimed); - - serviceReward.totalClaimed += total; - - // Claim children rewards - uint256[] memory children = IContributionNft(contributionNft) - .getChildren(nftId); - - uint256 totalChildrenAmount; - uint256 childAmount; - for (uint256 i = 0; i < children.length; i++) { - ServiceReward storage childReward = _serviceRewards[children[i]]; - - childAmount = (childReward.parentAmount - - childReward.totalClaimedParent); - - if (childAmount > 0) { - childReward.totalClaimedParent += childAmount; - total += childAmount; - totalChildrenAmount += childAmount; - } - } - - if (total == 0) { - return; - } - - IERC20(rewardToken).safeTransfer(account, total); - emit ServiceRewardsClaimed(nftId, account, total, totalChildrenAmount); - } - - function getTotalClaimableRewards( - address account, - uint256[] memory virtualIds, - uint256[] memory contributionNftIds - ) public view returns (uint256) { - uint256 total = 0; - for (uint256 i = 0; i < virtualIds.length; i++) { - total += - _getClaimableStakerRewards(account, virtualIds[i]) + - _getClaimableValidatorRewards(account, virtualIds[i]); - } - for (uint256 i = 0; i < contributionNftIds.length; i++) { - total += _getClaimableServiceRewards(contributionNftIds[i]); - } - return total; - } - - function claimAllRewards( - uint256[] memory virtualIds, - uint256[] memory contributionNftIds - ) public { - address account = _msgSender(); - for (uint256 i = 0; i < virtualIds.length; i++) { - _claimStakerRewards(account, virtualIds[i]); - _claimValidatorRewards(virtualIds[i]); - } - - for (uint256 i = 0; i < contributionNftIds.length; i++) { - _claimServiceRewards(contributionNftIds[i]); - } - } - - // ---------------- - // Manage parameters - // ---------------- - - function setRewardSettings( - uint16 protocolShares_, - uint16 contributorShares_, - uint16 stakerShares_, - uint16 parentShares_, - uint256 stakeThreshold_ - ) public onlyGov { - _rewardSettings.push( - SafeCast.toUint32(block.number), - RewardSettingsCheckpoints.RewardSettings( - protocolShares_, - contributorShares_, - stakerShares_, - parentShares_, - stakeThreshold_ - ) - ); - - emit RewardSettingsUpdated( - protocolShares_, - contributorShares_, - stakerShares_, - parentShares_, - stakeThreshold_ - ); - } - - function updateRefContracts( - address rewardToken_, - address agentNft_, - address contributionNft_, - address serviceNft_ - ) external onlyGov { - rewardToken = rewardToken_; - agentNft = agentNft_; - contributionNft = contributionNft_; - serviceNft = serviceNft_; - - emit RefContractsUpdated( - rewardToken_, - agentNft_, - contributionNft_, - serviceNft_ - ); - } -} diff --git a/contracts/RewardTreasury.sol.old b/contracts/RewardTreasury.sol.old deleted file mode 100644 index e366ee7..0000000 --- a/contracts/RewardTreasury.sol.old +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts/access/AccessControl.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./AgentReward.sol"; - -contract RewardTreasury is AccessControl { - using SafeERC20 for IERC20; - - struct RewardBucket { - uint256 amount; - bool disbursed; - } - - bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); - - mapping(uint32 => RewardBucket) public buckets; - IERC20 public rewardToken; - AgentReward public rewardManager; - - event RewardCreated( - uint256 total, - uint8 count, - uint32 interval, - uint32 startTS - ); - - event TokenSaved(address indexed by, address indexed token, uint256 amount); - - event RewardManagerUpdated(address newManager); - - event RewardDisbursed(uint32 indexed ts, uint256 amount); - - event RewardAmountUpdated(uint32 indexed ts, uint256 amount); - - constructor(address admin, address token, address rewardManager_) { - rewardToken = IERC20(token); - rewardToken.approve(rewardManager_, type(uint256).max); - rewardManager = AgentReward(rewardManager_); - _grantRole(DEFAULT_ADMIN_ROLE, admin); - } - - function createReward( - uint256 total, - uint8 count, - uint32 interval, - uint32 startTS - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(total > 0, "Total must be greater than zero"); - require(count > 0, "Number of buckets must be greater than zero"); - require(interval > 0, "Interval must be greater than zero"); - require( - startTS > block.timestamp, - "Start timestamp must be in the future" - ); - - uint256 singleReward = total / count; - for (uint8 i = 0; i < count; i++) { - uint32 ts = startTS + i * interval; - - RewardBucket storage bucket = buckets[ts]; - bucket.amount += singleReward; - } - - emit RewardCreated(total, count, interval, startTS); - } - - function saveToken( - address _token, - uint256 _amount - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - IERC20(_token).safeTransfer(_msgSender(), _amount); - emit TokenSaved(_msgSender(), _token, _amount); - } - - function withdraw(uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { - payable(_msgSender()).transfer(_amount); - } - - function distribute(uint32 ts) external onlyRole(EXECUTOR_ROLE) { - RewardBucket storage bucket = buckets[ts]; - require(!bucket.disbursed, "Bucket already disbursed"); - require(bucket.amount > 0, "Bucket is empty"); - require(block.timestamp >= ts, "Disbursement time not reached"); - require( - rewardToken.balanceOf(address(this)) >= bucket.amount, - "Insufficient balance" - ); - - bucket.disbursed = true; - rewardManager.distributeRewards(bucket.amount); - - emit RewardDisbursed(ts, bucket.amount); - } - - function setRewardManager( - address rewardManager_ - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - rewardManager = AgentReward(rewardManager_); - emit RewardManagerUpdated(rewardManager_); - } - - function adjustAmount( - uint32 ts, - uint256 amount - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - RewardBucket storage bucket = buckets[ts]; - require(!bucket.disbursed, "Bucket already disbursed"); - - bucket.amount = amount; - emit RewardAmountUpdated(ts, amount); - } -} diff --git a/scripts/arguments/rewardsV2.js b/scripts/arguments/rewardsV2.js new file mode 100644 index 0000000..aea1b00 --- /dev/null +++ b/scripts/arguments/rewardsV2.js @@ -0,0 +1,8 @@ +module.exports = [ + process.env.REWARD_TOKEN, + process.env.VIRTUAL_NFT, + { + protocolShares: process.env.PROTOCOL_SHARES, + stakerShares: process.env.STAKER_SHARES, + }, +]; diff --git a/scripts/v2/deployRewards.ts b/scripts/v2/deployRewards.ts new file mode 100644 index 0000000..2d7948d --- /dev/null +++ b/scripts/v2/deployRewards.ts @@ -0,0 +1,20 @@ +import { ethers, upgrades } from "hardhat"; + +const adminSigner = new ethers.Wallet( + process.env.ADMIN_PRIVATE_KEY, + ethers.provider +); + +(async () => { + try { + const args = require("../arguments/rewardsV2"); + const Contract = await ethers.getContractFactory("AgentRewardV2"); + const contract = await upgrades.deployProxy(Contract, args, { + initialOwner: process.env.CONTRACT_CONTROLLER, + }); + await contract.waitForDeployment(); + console.log("AgentRewardV2 deployed to:", contract.target); + } catch (e) { + console.log(e); + } +})(); From 45efda02c85e6488f6e54fff4344d4eb2962eec1 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 12 Jun 2024 18:55:27 +0800 Subject: [PATCH 25/30] upgraded migrate script --- contracts/dev/ProxyAdmin.sol | 7 +++++ contracts/virtualPersona/AgentNftV2.sol | 16 ++++++++++++ .../virtualPersona/ValidatorRegistry.sol | 10 +++++++ scripts/v2/migrateV2.ts | 26 +++++++++++-------- 4 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 contracts/dev/ProxyAdmin.sol diff --git a/contracts/dev/ProxyAdmin.sol b/contracts/dev/ProxyAdmin.sol new file mode 100644 index 0000000..68fb2b9 --- /dev/null +++ b/contracts/dev/ProxyAdmin.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +contract MyProxyAdmin is ProxyAdmin { + constructor(address initialOwner) ProxyAdmin(initialOwner) {} +} diff --git a/contracts/virtualPersona/AgentNftV2.sol b/contracts/virtualPersona/AgentNftV2.sol index 6ba9dd5..4e483a3 100644 --- a/contracts/virtualPersona/AgentNftV2.sol +++ b/contracts/virtualPersona/AgentNftV2.sol @@ -59,6 +59,10 @@ contract AgentNftV2 is _disableInitializers(); } + function kk() public pure returns (string memory) { + return "kk"; + } + function initialize(address defaultAdmin) public initializer { __ERC721_init("Agent", "AGENT"); __ERC721URIStorage_init(); @@ -148,6 +152,10 @@ contract AgentNftV2 is _initValidatorScore(virtualId, validator); } + function initValidatorScore(uint256 virtualId, address validator) public { + _initValidatorScore(virtualId, validator); + } + function _validatorScoreOf( uint256 virtualId, address account @@ -281,4 +289,12 @@ contract AgentNftV2 is ) public onlyRole(ADMIN_ROLE) { _blacklists[virtualId] = value; } + + function migrateScoreFunctions() public onlyRole(ADMIN_ROLE) { + _migrateScoreFunctions( + _validatorScoreOf, + totalProposals, + _getPastValidatorScore + ); + } } diff --git a/contracts/virtualPersona/ValidatorRegistry.sol b/contracts/virtualPersona/ValidatorRegistry.sol index e4612ea..2be7790 100644 --- a/contracts/virtualPersona/ValidatorRegistry.sol +++ b/contracts/virtualPersona/ValidatorRegistry.sol @@ -83,4 +83,14 @@ abstract contract ValidatorRegistry is IValidatorRegistry, Initializable { } return totalScore; } + + function _migrateScoreFunctions( + function(uint256, address) view returns (uint256) getScoreOf_, + function(uint256) view returns (uint256) getMaxScore_, + function(uint256, address, uint256) view returns (uint256) getPastScore_ + ) internal { + _getScoreOf = getScoreOf_; + _getMaxScore = getMaxScore_; + _getPastScore = getPastScore_; + } } diff --git a/scripts/v2/migrateV2.ts b/scripts/v2/migrateV2.ts index d3d51f9..efecbcf 100644 --- a/scripts/v2/migrateV2.ts +++ b/scripts/v2/migrateV2.ts @@ -10,7 +10,6 @@ const deployerSigner = new ethers.Wallet( ethers.provider ); - async function upgradeFactory(implementations, assetToken) { const AgentFactory = await ethers.getContractFactory("AgentFactoryV2"); const factory = await upgrades.upgradeProxy( @@ -71,9 +70,16 @@ async function updateAgentNftRoles() { } async function upgradeAgentNft() { - const Factory = await ethers.getContractFactory("AgentNftV2", adminSigner); - const factory = await upgrades.upgradeProxy(process.env.VIRTUAL_NFT, Factory); - console.log("Upgraded AgentNftV2", factory.target); + const nft = await ethers.deployContract("AgentNftV2"); + console.log("AgentNft deployed to:", nft.target); + + const proxyAdmin = await ethers.getContractAt( + "MyProxyAdmin", + process.env.VIRTUAL_NFT_PROXY, + adminSigner + ); + await proxyAdmin.upgradeAndCall(process.env.VIRTUAL_NFT, nft.target, "0x"); + console.log("Upgraded AgentNft"); } async function deployImplementations() { @@ -93,20 +99,18 @@ async function deployImplementations() { } async function deployMinter() { - const deployParams = require("../arguments/minter") + const deployParams = require("../arguments/minter"); const minter = await ethers.deployContract("Minter", deployParams); await minter.waitForDeployment(); console.log("Minter deployed to:", minter.target); } - (async () => { try { - // await deployMinter(); - // const implementations = await deployImplementations(); - // await upgradeFactory(implementations, process.env.BRIDGED_TOKEN); - await deployAgentNft(); - //await upgradeAgentNft(); + await deployMinter(); + const implementations = await deployImplementations(); + await upgradeFactory(implementations, process.env.BRIDGED_TOKEN); + await upgradeAgentNft(); } catch (e) { console.log(e); } From 06c2fccfd46e4f9f60225a2fb6e437db2cbd6cbc Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 13 Jun 2024 23:11:25 +0800 Subject: [PATCH 26/30] update with test cases --- contracts/token/IMinter.sol | 11 + contracts/token/Minter.sol | 65 +- contracts/virtualPersona/AgentNftV2.sol | 3 + contracts/virtualPersona/AgentVeToken.sol | 9 +- scripts/arguments/minter.js | 1 - test/agentDAO.js | 1 - test/contribution.js | 1105 +++++++++++---------- test/deprecated/contribution.js | 660 ++++++++++++ test/{ => deprecated}/staking.js | 0 test/rewardsV2.js | 54 - test/virtualGenesis.js | 98 +- 11 files changed, 1412 insertions(+), 595 deletions(-) create mode 100644 test/deprecated/contribution.js rename test/{ => deprecated}/staking.js (100%) diff --git a/contracts/token/IMinter.sol b/contracts/token/IMinter.sol index 8b711fa..1b14f5b 100644 --- a/contracts/token/IMinter.sol +++ b/contracts/token/IMinter.sol @@ -3,4 +3,15 @@ pragma solidity ^0.8.20; interface IMinter { function mint(uint256 nftId) external; + + event ImpactMultiplierUpdated(uint256 newMultiplier); + event AgentImpactMultiplierUpdated( + uint256 indexed virtualId, + uint256 newMultiplier + ); + event IPShareUpdated(uint256 newMultiplier); + event AgentIPShareUpdated( + uint256 indexed virtualId, + uint256 newMultiplier + ); } diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol index ccefa03..7a941ef 100644 --- a/contracts/token/Minter.sol +++ b/contracts/token/Minter.sol @@ -18,13 +18,15 @@ contract Minter is IMinter, Ownable { address public ipVault; uint256 public ipShare; // Share for IP holder - uint256 public dataShare; // Share for Dataset provider uint256 public impactMultiplier; uint256 public constant DENOM = 10000; mapping(uint256 => bool) _mintedNfts; + mapping(uint256 => uint256) public impactMulOverrides; + mapping(uint256 => uint256) public ipShareOverrides; + bool internal locked; modifier noReentrant() { @@ -34,6 +36,12 @@ contract Minter is IMinter, Ownable { locked = false; } + modifier onlyAgentDAO(uint256 virtualId) { + address daoAddress = IAgentNft(agentNft).virtualInfo(virtualId).dao; + require(daoAddress == _msgSender(), "Only Agent DAO can operate"); + _; + } + address agentFactory; constructor( @@ -41,7 +49,6 @@ contract Minter is IMinter, Ownable { address contributionAddress, address agentAddress, uint256 _ipShare, - uint256 _dataShare, uint256 _impactMultiplier, address _ipVault, address _agentFactory, @@ -51,7 +58,6 @@ contract Minter is IMinter, Ownable { contributionNft = contributionAddress; agentNft = agentAddress; ipShare = _ipShare; - dataShare = _dataShare; impactMultiplier = _impactMultiplier; ipVault = _ipVault; agentFactory = _agentFactory; @@ -70,12 +76,17 @@ contract Minter is IMinter, Ownable { contributionNft = contributionAddress; } - function setIpShare(uint256 _ipShare) public onlyOwner { + function setIPShare(uint256 _ipShare) public onlyOwner { ipShare = _ipShare; + emit IPShareUpdated(_ipShare); } - function setDataShare(uint256 _dataShare) public onlyOwner { - dataShare = _dataShare; + function setIPShareOverride( + uint256 virtualId, + uint256 _ipShare + ) public onlyAgentDAO(virtualId) { + ipShareOverrides[virtualId] = _ipShare; + emit AgentIPShareUpdated(virtualId, _ipShare); } function setIPVault(address _ipVault) public onlyOwner { @@ -88,6 +99,22 @@ contract Minter is IMinter, Ownable { function setImpactMultiplier(uint256 _multiplier) public onlyOwner { impactMultiplier = _multiplier; + emit ImpactMultiplierUpdated(_multiplier); + } + + function setImpactMulOverride(uint256 virtualId, uint256 mul) public onlyAgentDAO(virtualId) { + impactMulOverrides[virtualId] = mul; + emit AgentImpactMultiplierUpdated(virtualId, mul); + } + + function _getImpactMultiplier( + uint256 virtualId + ) internal view returns (uint256) { + uint256 mul = impactMulOverrides[virtualId]; + if (mul == 0) { + mul = impactMultiplier; + } + return mul; } function mint(uint256 nftId) public noReentrant { @@ -98,26 +125,28 @@ contract Minter is IMinter, Ownable { require(!_mintedNfts[nftId], "Already minted"); - uint256 agentId = IContributionNft(contributionNft).tokenVirtualId( + uint256 virtualId = IContributionNft(contributionNft).tokenVirtualId( nftId ); - require(agentId != 0, "Agent not found"); + require(virtualId != 0, "Agent not found"); _mintedNfts[nftId] = true; - address tokenAddress = IAgentNft(agentNft).virtualInfo(agentId).token; + address tokenAddress = IAgentNft(agentNft).virtualInfo(virtualId).token; IContributionNft contribution = IContributionNft(contributionNft); require(contribution.isModel(nftId), "Not a model contribution"); + uint256 finalImpactMultiplier = _getImpactMultiplier(virtualId); uint256 datasetId = contribution.getDatasetId(nftId); - uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * impactMultiplier * 10 ** 18) / DENOM; - uint256 ipAmount = (amount * ipShare) / DENOM; - uint256 dataAmount = 0; - - if (datasetId != 0) { - dataAmount = (amount * dataShare) / DENOM; - amount = amount - dataAmount; - } + uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * + finalImpactMultiplier * + 10 ** 18) / DENOM; + uint256 dataAmount = datasetId > 0 + ? (IServiceNft(serviceNft).getImpact(datasetId) * + finalImpactMultiplier * + 10 ** 18) / DENOM + : 0; + uint256 ipAmount = ((amount + dataAmount) * ipShare) / DENOM; // Mint to model owner if (amount > 0) { @@ -126,7 +155,7 @@ contract Minter is IMinter, Ownable { } // Mint to Dataset owner - if (datasetId != 0 && dataAmount > 0) { + if (datasetId != 0) { address datasetOwner = IERC721(contributionNft).ownerOf(datasetId); IAgentToken(tokenAddress).mint(datasetOwner, dataAmount); } diff --git a/contracts/virtualPersona/AgentNftV2.sol b/contracts/virtualPersona/AgentNftV2.sol index 4e483a3..2c9ed19 100644 --- a/contracts/virtualPersona/AgentNftV2.sol +++ b/contracts/virtualPersona/AgentNftV2.sol @@ -54,6 +54,8 @@ contract AgentNftV2 is mapping(uint256 => bool) private _blacklists; mapping(uint256 => VirtualLP) public virtualLPs; + event AgentBlacklisted(uint256 indexed virtualId, bool value); + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -288,6 +290,7 @@ contract AgentNftV2 is bool value ) public onlyRole(ADMIN_ROLE) { _blacklists[virtualId] = value; + emit AgentBlacklisted(virtualId, value); } function migrateScoreFunctions() public onlyRole(ADMIN_ROLE) { diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol index d2a4fd5..436edee 100644 --- a/contracts/virtualPersona/AgentVeToken.sol +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -73,12 +73,15 @@ contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { "Insufficient asset token allowance" ); + IAgentNft registry = IAgentNft(agentNft); + uint256 virtualId = registry.stakingTokenToVirtualId(address(this)); + + require(!registry.isBlacklisted(virtualId), "Agent Blacklisted"); + if (totalSupply() == 0) { initialLock = amount; } - - IAgentNft registry = IAgentNft(agentNft); - uint256 virtualId = registry.stakingTokenToVirtualId(address(this)); + registry.addValidator(virtualId, delegatee); IERC20(assetToken).safeTransferFrom(sender, address(this), amount); diff --git a/scripts/arguments/minter.js b/scripts/arguments/minter.js index e621e4c..eb66b4a 100644 --- a/scripts/arguments/minter.js +++ b/scripts/arguments/minter.js @@ -3,7 +3,6 @@ module.exports = [ process.env.CONTRIBUTION_NFT, process.env.VIRTUAL_NFT, process.env.IP_SHARES, - process.env.DATA_SHARES, process.env.IMPACT_MULTIPLIER, process.env.IP_VAULT, process.env.VIRTUAL_FACTORY, diff --git a/test/agentDAO.js b/test/agentDAO.js index 7a3aed4..09c491f 100644 --- a/test/agentDAO.js +++ b/test/agentDAO.js @@ -128,7 +128,6 @@ describe("AgentDAO", function () { contribution.target, agentNft.target, process.env.IP_SHARES, - process.env.DATA_SHARES, process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, diff --git a/test/contribution.js b/test/contribution.js index e4110aa..b844d3a 100644 --- a/test/contribution.js +++ b/test/contribution.js @@ -1,12 +1,3 @@ -/* -We will test the end-to-end implementation of a Contribution flow till Service. - -1. Prepare 100k tokens -2. Propose a new Persona at AgentFactory -3. Once received proposalId from AgentFactory, create a proposal at ProtocolDAO -4. Vote on the proposal -5. Execute the proposal -*/ const { parseEther, formatEther, toBeHex } = require("ethers/utils"); const { ethers } = require("hardhat"); const abi = ethers.AbiCoder.defaultAbiCoder(); @@ -16,10 +7,18 @@ const { mine, } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); -const getExecuteCallData = (factory, proposalId) => { - return factory.interface.encodeFunctionData("executeApplication", [ - proposalId, - false +const getSetImpactMulCalldata = async (minter, virtualId, multiplier) => { + return minter.interface.encodeFunctionData("setImpactMulOverride", [ + virtualId, + multiplier, + ]); +}; + + +const getSetIPShareCalldata = async (minter, virtualId, share) => { + return minter.interface.encodeFunctionData("setIPShareOverride", [ + virtualId, + share, ]); }; @@ -28,211 +27,291 @@ const getMintServiceCalldata = async (serviceNft, virtualId, hash) => { }; describe("Contribution", function () { - const PROPOSAL_THRESHOLD = parseEther("100000"); - const CONTRIBUTION_DESC = "LLM Model #1001"; - const TOKEN_URI = "http://virtuals.io"; + const PROPOSAL_THRESHOLD = parseEther("100000"); //100k + const TREASURY_AMOUNT = parseEther("1000000"); //1M + const TOKEN_URI = "http://jessica"; + + const genesisInput = { + name: "Jessica", + symbol: "JSC", + tokenURI: "http://jessica", + daoName: "Jessica DAO", + cores: [0, 1, 2], + tbaSalt: + "0xa7647ac9429fdce477ebd9a95510385b756c757c26149e740abbab0ad1be2f16", + tbaImplementation: process.env.TBA_IMPLEMENTATION, + daoVotingPeriod: 600, + daoThreshold: 1000000000000000000000n, + }; + + const getAccounts = async () => { + const [ + deployer, + ipVault, + founder, + contributor1, + contributor2, + validator1, + validator2, + treasury, + virtualTreasury, + trader, + ] = await ethers.getSigners(); + return { + deployer, + ipVault, + founder, + contributor1, + contributor2, + validator1, + validator2, + treasury, + virtualTreasury, + trader, + }; + }; async function deployBaseContracts() { - const signers = await ethers.getSigners(); - const [deployer] = signers; - const veToken = await ethers.deployContract( - "veVirtualToken", - [deployer.address], + const { deployer, ipVault, treasury, virtualTreasury } = + await getAccounts(); + + const virtualToken = await ethers.deployContract( + "VirtualToken", + [TREASURY_AMOUNT, deployer.address], {} ); - await veToken.waitForDeployment(); + await virtualToken.waitForDeployment(); + + const AgentNft = await ethers.getContractFactory("AgentNftV2"); + const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); - const demoToken = await ethers.deployContract( - "BMWToken", - [deployer.address], + const contribution = await upgrades.deployProxy( + await ethers.getContractFactory("ContributionNft"), + [agentNft.target], {} ); - await demoToken.waitForDeployment(); - const protocolDAO = await ethers.deployContract( - "VirtualGenesisDAO", - [veToken.target, 0, 100, 0], + const service = await upgrades.deployProxy( + await ethers.getContractFactory("ServiceNft"), + [agentNft.target, contribution.target, process.env.DATASET_SHARES], {} ); - await protocolDAO.waitForDeployment(); - const AgentNft = await ethers.getContractFactory("AgentNft"); - const personaNft = await upgrades.deployProxy(AgentNft, [deployer.address]); + await agentNft.setContributionService(contribution.target, service.target); - const personaToken = await ethers.deployContract("AgentToken"); - await personaToken.waitForDeployment(); - const personaDAO = await ethers.deployContract("AgentDAO"); - await personaDAO.waitForDeployment(); + // Implementation contracts + const agentToken = await ethers.deployContract("AgentToken"); + await agentToken.waitForDeployment(); + const agentDAO = await ethers.deployContract("AgentDAO"); + await agentDAO.waitForDeployment(); + const agentVeToken = await ethers.deployContract("AgentVeToken"); + await agentVeToken.waitForDeployment(); - const tba = await ethers.deployContract("ERC6551Registry"); - - const personaFactory = await upgrades.deployProxy( - await ethers.getContractFactory("AgentFactory"), + const agentFactory = await upgrades.deployProxy( + await ethers.getContractFactory("AgentFactoryV2"), [ - personaToken.target, - personaDAO.target, - tba.target, - demoToken.target, - personaNft.target, + agentToken.target, + agentVeToken.target, + agentDAO.target, + process.env.TBA_REGISTRY, + virtualToken.target, + agentNft.target, PROPOSAL_THRESHOLD, - 5, - protocolDAO.target, deployer.address, ] ); - - await personaNft.grantRole( - await personaNft.MINTER_ROLE(), - personaFactory.target - ); - - const contribution = await upgrades.deployProxy( - await ethers.getContractFactory("ContributionNft"), - [personaNft.target], - {} - ); - - const service = await upgrades.deployProxy( - await ethers.getContractFactory("ServiceNft"), - [personaNft.target, contribution.target, process.env.DATASET_SHARES], - {} - ); - - await personaNft.setContributionService( + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + const minter = await ethers.deployContract("Minter", [ + service.target, contribution.target, - service.target + agentNft.target, + process.env.IP_SHARES, + process.env.IMPACT_MULTIPLIER, + ipVault.address, + agentFactory.target, + deployer.address + ]); + await minter.waitForDeployment(); + await agentFactory.setMinter(minter.target); + await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years + await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); + await agentFactory.setTokenAdmin(deployer.address); + await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION + ); + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address + ); + await agentFactory.grantRole( + await agentFactory.WITHDRAW_ROLE(), + deployer.address + ); + + const rewards = await upgrades.deployProxy( + await ethers.getContractFactory("AgentRewardV2"), + [ + virtualToken.target, + agentNft.target, + { + protocolShares: process.env.PROTOCOL_SHARES, + stakerShares: process.env.STAKER_SHARES, + }, + ], + {} ); + await rewards.waitForDeployment(); + await rewards.grantRole(await rewards.GOV_ROLE(), deployer.address); return { - veToken, - protocolDAO, - demoToken, - personaFactory, - personaNft, - contribution, - service, + virtualToken, + agentFactory, + agentNft, + serviceNft: service, + contributionNft: contribution, + minter, + rewards, }; } - async function deployGenesisVirtual() { - const genesisInput = { - name: "Jessica", - symbol: "JSC", - tokenURI: "http://jessica", - daoName: "Jessica DAO", - cores: [0, 1, 2], - tbaSalt: - "0xa7647ac9429fdce477ebd9a95510385b756c757c26149e740abbab0ad1be2f16", - tbaImplementation: ethers.ZeroAddress, - daoVotingPeriod: 600, - daoThreshold: 1000000000000000000000n, - }; - - const contracts = await deployBaseContracts(); - const { personaFactory, veToken, protocolDAO, demoToken } = contracts; - const [deployer] = await ethers.getSigners(); + async function createApplication(base, founder, idx) { + const { agentFactory, virtualToken } = base; // Prepare tokens for proposal - await demoToken.mint(deployer.address, PROPOSAL_THRESHOLD); - await demoToken.approve(personaFactory.target, PROPOSAL_THRESHOLD); - - await personaFactory.proposePersona( - genesisInput.name, - genesisInput.symbol, - genesisInput.tokenURI, - genesisInput.cores, - genesisInput.tbaSalt, - genesisInput.tbaImplementation, - genesisInput.daoVotingPeriod, - genesisInput.daoThreshold - ); - - const filter = personaFactory.filters.NewApplication; - const events = await personaFactory.queryFilter(filter, -1); + await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); + await virtualToken + .connect(founder) + .approve(agentFactory.target, PROPOSAL_THRESHOLD); + const tx = await agentFactory + .connect(founder) + .proposeAgent( + genesisInput.name + "-" + idx, + genesisInput.symbol, + genesisInput.tokenURI, + genesisInput.cores, + genesisInput.tbaSalt, + genesisInput.tbaImplementation, + genesisInput.daoVotingPeriod, + genesisInput.daoThreshold + ); + + const filter = agentFactory.filters.NewApplication; + const events = await agentFactory.queryFilter(filter, -1); const event = events[0]; const { id } = event.args; + return id; + } - // Create proposal - await veToken.oracleTransfer( - [ethers.ZeroAddress], - [deployer.address], - [parseEther("100000000")] - ); - await veToken.delegate(deployer.address); - - await protocolDAO.propose( - [personaFactory.target], - [0], - [getExecuteCallData(personaFactory, id)], - "Create Jessica" - ); + async function deployWithApplication() { + const base = await deployBaseContracts(); - const daoFilter = protocolDAO.filters.ProposalCreated; - const daoEvents = await protocolDAO.queryFilter(daoFilter, -1); - const daoEvent = daoEvents[0]; - const daoProposalId = daoEvent.args[0]; + const { founder } = await getAccounts(); + const id = await createApplication(base, founder, 0); + return { applicationId: id, ...base }; + } - await protocolDAO.castVote(daoProposalId, 1); - await mine(600); + async function createAgent(base, applicationId) { + const { agentFactory } = base; + await agentFactory.executeApplication(applicationId, true); - await protocolDAO.execute(daoProposalId); - const factoryFilter = personaFactory.filters.NewPersona; - const factoryEvents = await personaFactory.queryFilter(factoryFilter, -1); + const factoryFilter = agentFactory.filters.NewPersona; + const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); const factoryEvent = factoryEvents[0]; + return factoryEvent.args; + } + + async function deployWithAgent() { + const base = await deployWithApplication(); + const { applicationId } = base; + + const { founder } = await getAccounts(); + + const { virtualId, token, veToken, dao, tba, lp } = await createAgent( + base, + applicationId + ); + + const veTokenContract = await ethers.getContractAt("AgentVeToken", veToken); + await veTokenContract.connect(founder).delegate(founder.address); // We want to vote instead of letting default delegatee to vote - const { virtualId, token, dao, tba } = factoryEvent.args; - const persona = { virtualId, token, dao, tba }; - return { ...contracts, persona }; + return { + ...base, + agent: { + virtualId, + token, + veToken, + dao, + tba, + lp, + }, + }; } - async function proposeContribution() { - const signers = await ethers.getSigners(); - const base = await loadFixture(deployGenesisVirtual); - const { persona, personaNft, service, demoToken } = base; - const descHash = getDescHash(CONTRIBUTION_DESC); + async function createContribution( + virtualId, + coreId, + maturity, + parentId, + isModel, + datasetId, + desc, + base, + account, + voters + ) { + const { serviceNft, contributionNft, minter, agentNft } = base; + const daoAddr = (await agentNft.virtualInfo(virtualId)).dao; + const veAddr = (await agentNft.virtualLP(virtualId)).veToken; + const agentDAO = await ethers.getContractAt("AgentDAO", daoAddr); + const veToken = await ethers.getContractAt("AgentVeToken", veAddr); + + const descHash = getDescHash(desc); - const personaDaoContract = await ethers.getContractFactory("AgentDAO"); const mintCalldata = await getMintServiceCalldata( - service, - persona.virtualId, + serviceNft, + virtualId, descHash ); - // Prepare tokens for other validator - const [validator1, validator2, validator3] = signers; - await demoToken.mint(validator2.address, parseEther("50000")); - await demoToken - .connect(validator2) - .approve(persona.token, parseEther("50000")); - await demoToken.mint(validator3.address, parseEther("70000")); - await demoToken - .connect(validator3) - .approve(persona.token, parseEther("70000")); - // // Set as validator - await personaNft.addValidator(persona.virtualId, validator2.address); - await personaNft.addValidator(persona.virtualId, validator3.address); - const tokenInstance = await ethers.getContractAt( - "AgentToken", - persona.token - ); - await tokenInstance - .connect(validator2) - .stake(parseEther("50000"), validator2.address, validator2.address); - await tokenInstance - .connect(validator3) - .stake(parseEther("70000"), validator3.address, validator3.address); - - await personaDaoContract - .attach(persona.dao) - .propose([service.target], [0], [mintCalldata], CONTRIBUTION_DESC); - const filter = personaDaoContract.attach(persona.dao).filters - .ProposalCreated; - const events = await personaDaoContract - .attach(persona.dao) - .queryFilter(filter, -1); + await agentDAO.propose([serviceNft.target], [0], [mintCalldata], desc); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); const event = events[0]; const proposalId = event.args[0]; - return { ...base, proposalId }; + + await contributionNft.mint( + account, + virtualId, + coreId, + TOKEN_URI, + proposalId, + parentId, + isModel, + datasetId + ); + const voteParams = isModel + ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) + : "0x"; + + for (const voter of voters) { + await agentDAO + .connect(voter) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + } + + await mine(600); + + await agentDAO.execute(proposalId); + if (isModel) { + await minter.mint(proposalId); + } + + return proposalId; } function getDescHash(str) { @@ -246,406 +325,406 @@ describe("Contribution", function () { }); it("should be able to mint a new contribution", async function () { - const { proposalId, contribution, persona } = await loadFixture( - proposeContribution + const base = await loadFixture(deployWithAgent); + const { contributor1, founder, contributior1 } = await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token ); + const { contributionNft, agent } = base; + const veAddr = agent.veToken; + const veToken = await ethers.getContractAt("AgentVeToken", veAddr); + await veToken.connect(founder).delegate(founder.address); + const balance1 = await contributionNft.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); - // Mint contribution - await expect( - contribution.mint( - this.accounts[1], - persona.virtualId, - 0, - TOKEN_URI, - proposalId, - "0x0000000000000000000000000000000000000000", - true, - 0 - ) - ).to.emit(contribution, "NewContribution"); + const contributionId = await createContribution( + 1, + 0, + maturity, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [founder] + ); - expect(await contribution.ownerOf(proposalId)).to.be.equal( - this.accounts[1] + const balance2 = await contributionNft.balanceOf(contributor1.address); + expect(balance2).to.equal(1n); + expect(await contributionNft.ownerOf(contributionId)).to.be.equal( + contributor1.address ); }); - it("should mint service nft once proposal accepted", async function () { - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + it("should mint agent token for successful model contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1, founder } = await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token ); - - await contribution.mint( - this.accounts[1], - persona.virtualId, + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); + await createContribution( + 1, + 0, + maturity, 0, - TOKEN_URI, - proposalId, - "0x0000000000000000000000000000000000000000", true, - 0 - ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); - // We need 51% to reach quorum - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [20, [0, 1, 1, 0, 2]] - ); - - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 1, - "lfg", - voteParams + 0, + "Test", + base, + contributor1.address, + [founder] ); - await personaDAO - .connect(this.signers[2]) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); - await mine(600); - await personaDAO.execute(proposalId); - - expect(await service.ownerOf(proposalId)).to.be.equal(persona.tba); - expect(await service.tokenURI(proposalId)).to.be.equal(TOKEN_URI); + const balance2 = await agentToken.balanceOf(contributor1.address); + expect(balance2).to.equal(parseEther(maturity.toString())); }); - it("should set correct maturity score", async function () { - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + it("should mint agent token for IP owner on successful contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1, founder, agentVeToken } = + await getAccounts(); + const maturity = 55; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token ); - await contribution.mint( - this.accounts[1], - persona.virtualId, + const balance1 = await agentToken.balanceOf(ipVault.address); + expect(balance1).to.equal(0n); + await await createContribution( + 1, + 0, + maturity, 0, - TOKEN_URI, - proposalId, - "0x0000000000000000000000000000000000000000", true, - 0 - ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); - /* - Scenario: - 1. Validator1 with 100000 votes set maturity score to 1500 - 2. Validator2 with 50000 votes set maturity score to 2000 - 3. Validator3 with 70000 votes set maturity score to 3000 - 4. Maturity = (1500 * 100000 + 2000 * 50000 + 3000 * 50000) / 220000 = 2090.9090 - */ - const [validator1, validator2, validator3] = this.signers; - const personaToken = await ethers.getContractAt( - "AgentToken", - persona.token - ); - expect( - formatEther(await personaToken.getVotes(validator1.address)) - ).to.be.equal("100000.0"); - expect( - formatEther(await personaToken.getVotes(validator2.address)) - ).to.be.equal("50000.0"); - expect( - formatEther(await personaToken.getVotes(validator3.address)) - ).to.be.equal("70000.0"); - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [1500, [0, 1, 1, 0, 2]] - ); - const voteParams2 = abi.encode( - ["uint256", "uint8[] memory"], - [2000, [0, 1, 1, 0, 2]] - ); - const voteParams3 = abi.encode( - ["uint256", "uint8[] memory"], - [3000, [0, 1, 1, 0, 2]] + 0, + "Test", + base, + contributor1.address, + [founder] ); - await expect( - personaDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams) - ) - .to.emit(personaDAO, "ValidatorEloRating") - .withArgs(proposalId, validator1.address, 1500, [0, 1, 1, 0, 2]); - await personaDAO - .connect(validator2) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams2); - await personaDAO - .connect(validator3) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams3); - await mine(43200 * 7); - await personaDAO.execute(proposalId); - expect(await service.getMaturity(proposalId)).to.be.equal(2090n); - expect(await service.getImpact(proposalId)).to.be.equal(2090n); + const balance2 = await agentToken.balanceOf(ipVault.address); + expect(balance2).to.equal(parseEther((maturity * 0.1).toString())); }); - it("should show correct impact score", async function () { - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + it("should mint agent token for model & dataset contribution", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1, contributor2, founder, agentVeToken } = + await getAccounts(); + const { contributionNft, serviceNft } = base; + const maturity = 100; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token ); - await contribution.mint( - this.accounts[1], - persona.virtualId, - 0, - TOKEN_URI, - proposalId, - "0x0000000000000000000000000000000000000000", - true, - 0 - ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); - /* - Scenario: - Continuing from previous test case, the first service NFT has maturity score of 2090 and we are improving it to 4000, the impact should be 4000-2090 = 1910 - */ - // Proposal 1 - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [1500, [0, 1, 1, 0, 2]] - ); - const voteParams2 = abi.encode( - ["uint256", "uint8[] memory"], - [2000, [0, 1, 1, 0, 2]] - ); - const voteParams3 = abi.encode( - ["uint256", "uint8[] memory"], - [3000, [0, 1, 1, 0, 2]] - ); - const voteParams4 = abi.encode( - ["uint256", "uint8[] memory"], - [4000, [0, 1, 1, 0, 2]] - ); - const [validator1, validator2, validator3] = this.signers; - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 1, - "lfg", - voteParams - ); - await personaDAO - .connect(validator2) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams2); - await personaDAO - .connect(validator3) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams3); - await mine(43200 * 7); - await personaDAO.execute(proposalId); - - // Proposal 2 - const descHash = getDescHash(CONTRIBUTION_DESC + " V2"); - - const personaDaoContract = await ethers.getContractFactory("AgentDAO"); - const mintCalldata = await getMintServiceCalldata( - service, - persona.virtualId, - descHash + const veToken = await ethers.getContractAt( + "AgentVeToken", + base.agent.veToken ); - await personaDaoContract - .attach(persona.dao) - .propose( - [service.target], - [0], - [mintCalldata], - CONTRIBUTION_DESC + " V2" - ); - const filter = personaDaoContract.attach(persona.dao).filters - .ProposalCreated; - const events = await personaDaoContract - .attach(persona.dao) - .queryFilter(filter, -1); - const event = events[0]; - const proposalId2 = event.args[0]; - await contribution.mint( - this.accounts[1], - persona.virtualId, + const agentDAO = await ethers.getContractAt("AgentDAO", base.agent.dao); + + // No agent token minted for dataset contribution + const c1 = await createContribution( + 1, 0, - TOKEN_URI, - proposalId2, - proposalId, - true, - 0 + maturity, + 0, + false, + 0, + "Dataset", + base, + contributor1.address, + [founder] ); + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); - await personaDAO.castVoteWithReasonAndParams( - proposalId2, + const c2 = await createContribution( 1, - "lfg", - voteParams4 - ); - await personaDAO - .connect(validator2) - .castVoteWithReasonAndParams(proposalId2, 1, "lfg", voteParams4); - await personaDAO - .connect(validator3) - .castVoteWithReasonAndParams(proposalId2, 1, "lfg", voteParams4); - await mine(43200 * 7); - await personaDAO.execute(proposalId2); - - expect(await service.getMaturity(proposalId2)).to.be.equal(4000n); - expect(await service.getImpact(proposalId2)).to.be.equal(1910n); - }); - - it("should allow contribution admin to create proposal", async () => { - const signers = await ethers.getSigners(); - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + 0, + maturity, + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder] ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); - await contribution.setAdmin(signers[15].address); - await expect( - personaDAO - .connect(signers[15]) - .propose([personaDAO.target], [0], [ethers.ZeroAddress], "Test1") - ).to.emit(personaDAO, "ProposalCreated"); - - await expect( - personaDAO - .connect(signers[14]) - .propose([personaDAO.target], [0], [ethers.ZeroAddress], "Test2") - ).to.be.reverted; - await contribution.connect(signers[15]).setAdmin(signers[14].address); + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("30")); - await expect( - personaDAO - .connect(signers[14]) - .propose([personaDAO.target], [0], [ethers.ZeroAddress], "Test3") - ).to.emit(personaDAO, "ProposalCreated"); + const balance12 = await agentToken.balanceOf(contributor1.address); + expect(balance12).to.equal(parseEther("70")); }); - it("should increase validator score with for,against,abstain", async function () { - const signers = await ethers.getSigners(); - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + it("should allow adjusting global agent token multiplier", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1, contributor2, founder, agentVeToken } = + await getAccounts(); + const { contributionNft, serviceNft, minter } = base; + const maturity = 100; + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + await minter.setImpactMultiplier(20000); //2x - const [validator1, validator2, validator3] = signers; - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [20, [0, 1, 1, 0, 2]] + // No agent token minted for dataset contribution + const c1 = await createContribution( + 1, + 0, + maturity, + 0, + false, + 0, + "Dataset", + base, + contributor1.address, + [founder] ); + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); - expect(await personaDAO.scoreOf(validator2)).to.be.equal(0); - expect(await personaDAO.scoreOf(validator3)).to.be.equal(0); - await personaDAO.castVoteWithReasonAndParams( - proposalId, + const c2 = await createContribution( + 1, 0, - "lfg", - voteParams - ); - await personaDAO - .connect(validator2) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); - await personaDAO - .connect(validator3) - .castVoteWithReasonAndParams(proposalId, 2, "lfg", voteParams); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(1n); - expect(await personaDAO.scoreOf(validator2)).to.be.equal(1n); - expect(await personaDAO.scoreOf(validator3)).to.be.equal(1n); + maturity, + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder] + ); + + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("60")); + + const balance12 = await agentToken.balanceOf(contributor1.address); + expect(balance12).to.equal(parseEther("140")); }); - it("should reject double votes", async function () { - const signers = await ethers.getSigners(); - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + it("should allow adjusting agent token multiplier", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1, contributor2, founder, agentVeToken } = + await getAccounts(); + const { contributionNft, serviceNft, minter, agent } = base; + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const maturity = 100; + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + + await veToken.connect(founder).delegate(founder.address); + + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + + const calldata = await getSetImpactMulCalldata( + minter, + agent.virtualId, + 20000n ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); - const [validator1, validator2, validator3] = signers; - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [20, [0, 1, 1, 0, 2]] + await agentDAO.propose( + [minter.target], + [0], + [calldata], + "Set agent token multiplier" ); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); + const event = events[0]; + const proposalId = event.args[0]; - await personaDAO.castVoteWithReasonAndParams( - proposalId, + await mine(1); + await agentDAO.connect(founder).castVote(proposalId, 1); + await mine(1); + await agentDAO.execute(proposalId); + + // No agent token minted for dataset contribution + const c1 = await createContribution( + 1, 0, - "lfg", - voteParams + maturity, + 0, + false, + 0, + "Dataset", + base, + contributor1.address, + [founder] ); - await expect( - personaDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams) - ).to.be.reverted; - }); + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); - it("should not increase validator score with deliberate votes", async function () { - const signers = await ethers.getSigners(); - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + const c2 = await createContribution( + 1, + 0, + maturity, + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder] ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); - const [validator1, validator2, validator3] = signers; - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [20, [0, 1, 1, 0, 2]] - ); + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("60")); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 3, - "lfg", - voteParams - ); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); + const balance12 = await agentToken.balanceOf(contributor1.address); + expect(balance12).to.equal(parseEther("140")); }); - it("should not increase validator score with deliberate votes after a valid vote", async function () { - const signers = await ethers.getSigners(); - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution - ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + it("should not allow adjusting agent token multiplier by public", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1, contributor2, founder, agentVeToken } = + await getAccounts(); + const { deployer, minter, agent } = base; + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const maturity = 100; + const agentToken = await ethers.getContractAt("AgentToken", agent.token); - const [validator1, validator2, validator3] = signers; - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [20, [0, 1, 1, 0, 2]] - ); + await expect( + minter.setImpactMulOverride(agent.virtualId, 20000) + ).to.be.revertedWith("Only Agent DAO can operate"); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); - await personaDAO.castVoteWithReasonAndParams( - proposalId, + // No agent token minted for dataset contribution + const c1 = await createContribution( 1, - "lfg", - voteParams + 0, + maturity, + 0, + false, + 0, + "Dataset", + base, + contributor1.address, + [founder] ); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(1n); - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 3, - "lfg", - voteParams + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); + + const c2 = await createContribution( + 1, + 0, + maturity, + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder] ); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(1n); + + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("30")); + + const balance12 = await agentToken.balanceOf(contributor1.address); + expect(balance12).to.equal(parseEther("70")); }); - it("should not emit ValidatorEloRating for deliberate votes", async function () { - const signers = await ethers.getSigners(); - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution - ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + it("should be able to adjust the global IP share", async function () { + const base = await loadFixture(deployWithAgent); + const { deployer, contributor1, ipVault, founder, agentVeToken } = + await getAccounts(); + const { serviceNft, minter, agent } = base; + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const maturity = 100; + const agentToken = await ethers.getContractAt("AgentToken", agent.token); - // Only contribution proposal will emit ValidatorEloRating event - await contribution.mint( - this.accounts[1], - persona.virtualId, + await minter.connect(deployer).setIPShare(5000); + + await createContribution( + 1, + 0, + maturity, 0, - TOKEN_URI, - proposalId, - "0x0000000000000000000000000000000000000000", true, - 0 + 0, + "Test model", + base, + contributor1.address, + [founder] ); - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [20, [0, 1, 1, 0, 2]] - ); + const balance = await agentToken.balanceOf(ipVault.address); + expect(balance).to.equal(parseEther("50")); + }); - await expect( - personaDAO.castVoteWithReasonAndParams(proposalId, 3, "lfg", voteParams) - ).to.not.to.emit(personaDAO, "ValidatorEloRating"); - await expect( - personaDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams) - ).to.emit(personaDAO, "ValidatorEloRating"); + it("should be able to adjust the IP share per agent", async function () { + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1, contributor2, founder, agentVeToken } = + await getAccounts(); + const { contributionNft, serviceNft, minter, agent } = base; + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const maturity = 100; + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + + await veToken.connect(founder).delegate(founder.address); + + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + + const calldata = await getSetIPShareCalldata( + minter, + agent.virtualId, + 1000n + ); + + await agentDAO.propose( + [minter.target], + [0], + [calldata], + "Set agent token multiplier" + ); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); + const event = events[0]; + const proposalId = event.args[0]; + + await mine(1); + await agentDAO.connect(founder).castVote(proposalId, 1); + await mine(1); + await agentDAO.execute(proposalId); + + // No agent token minted for dataset contribution + const c1 = await createContribution( + 1, + 0, + maturity, + 0, + false, + 0, + "Dataset", + base, + contributor1.address, + [founder] + ); + + const c2 = await createContribution( + 1, + 0, + maturity, + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder] + ); + + const balance = await agentToken.balanceOf(ipVault.address); + expect(balance).to.equal(parseEther("10")); + }); }); diff --git a/test/deprecated/contribution.js b/test/deprecated/contribution.js new file mode 100644 index 0000000..e709025 --- /dev/null +++ b/test/deprecated/contribution.js @@ -0,0 +1,660 @@ +const { parseEther, formatEther, toBeHex } = require("ethers/utils"); +const { ethers } = require("hardhat"); +const abi = ethers.AbiCoder.defaultAbiCoder(); +const { expect } = require("chai"); +const { + loadFixture, + mine, +} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); + +const getExecuteCallData = (factory, proposalId) => { + return factory.interface.encodeFunctionData("executeApplication", [ + proposalId, + false, + ]); +}; + +const getMintServiceCalldata = async (serviceNft, virtualId, hash) => { + return serviceNft.interface.encodeFunctionData("mint", [virtualId, hash]); +}; + +describe("Contribution", function () { + const PROPOSAL_THRESHOLD = parseEther("100000"); //100k + const TREASURY_AMOUNT = parseEther("1000000"); //1M + const TOKEN_URI = "http://jessica"; + + const genesisInput = { + name: "Jessica", + symbol: "JSC", + tokenURI: "http://jessica", + daoName: "Jessica DAO", + cores: [0, 1, 2], + tbaSalt: + "0xa7647ac9429fdce477ebd9a95510385b756c757c26149e740abbab0ad1be2f16", + tbaImplementation: process.env.TBA_IMPLEMENTATION, + daoVotingPeriod: 600, + daoThreshold: 1000000000000000000000n, + }; + + const getAccounts = async () => { + const [ + deployer, + ipVault, + founder, + contributor1, + contributor2, + validator1, + validator2, + treasury, + virtualTreasury, + trader, + ] = await ethers.getSigners(); + return { + deployer, + ipVault, + founder, + contributor1, + contributor2, + validator1, + validator2, + treasury, + virtualTreasury, + trader, + }; + }; + + async function deployBaseContracts() { + const { deployer, ipVault, treasury, virtualTreasury } = + await getAccounts(); + + const virtualToken = await ethers.deployContract( + "VirtualToken", + [TREASURY_AMOUNT, deployer.address], + {} + ); + await virtualToken.waitForDeployment(); + + const AgentNft = await ethers.getContractFactory("AgentNftV2"); + const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); + + const contribution = await upgrades.deployProxy( + await ethers.getContractFactory("ContributionNft"), + [agentNft.target], + {} + ); + + const service = await upgrades.deployProxy( + await ethers.getContractFactory("ServiceNft"), + [agentNft.target, contribution.target, process.env.DATASET_SHARES], + {} + ); + + await agentNft.setContributionService(contribution.target, service.target); + + // Implementation contracts + const agentToken = await ethers.deployContract("AgentToken"); + await agentToken.waitForDeployment(); + const agentDAO = await ethers.deployContract("AgentDAO"); + await agentDAO.waitForDeployment(); + const agentVeToken = await ethers.deployContract("AgentVeToken"); + await agentVeToken.waitForDeployment(); + + const agentFactory = await upgrades.deployProxy( + await ethers.getContractFactory("AgentFactoryV2"), + [ + agentToken.target, + agentVeToken.target, + agentDAO.target, + process.env.TBA_REGISTRY, + virtualToken.target, + agentNft.target, + PROPOSAL_THRESHOLD, + deployer.address, + ] + ); + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + const minter = await ethers.deployContract("Minter", [ + service.target, + contribution.target, + agentNft.target, + process.env.IP_SHARES, + process.env.DATA_SHARES, + process.env.IMPACT_MULTIPLIER, + ipVault.address, + agentFactory.target, + deployer.address, + ]); + await minter.waitForDeployment(); + await agentFactory.setMinter(minter.target); + await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years + await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); + await agentFactory.setTokenAdmin(deployer.address); + await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION + ); + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address + ); + await agentFactory.grantRole( + await agentFactory.WITHDRAW_ROLE(), + deployer.address + ); + + const rewards = await upgrades.deployProxy( + await ethers.getContractFactory("AgentRewardV2"), + [ + virtualToken.target, + agentNft.target, + { + protocolShares: process.env.PROTOCOL_SHARES, + stakerShares: process.env.STAKER_SHARES, + }, + ], + {} + ); + await rewards.waitForDeployment(); + await rewards.grantRole(await rewards.GOV_ROLE(), deployer.address); + + return { + virtualToken, + agentFactory, + agentNft, + serviceNft: service, + contributionNft: contribution, + minter, + rewards, + }; + } + + async function createApplication(base, founder, idx) { + const { agentFactory, virtualToken } = base; + + // Prepare tokens for proposal + await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); + await virtualToken + .connect(founder) + .approve(agentFactory.target, PROPOSAL_THRESHOLD); + const tx = await agentFactory + .connect(founder) + .proposeAgent( + genesisInput.name + "-" + idx, + genesisInput.symbol, + genesisInput.tokenURI, + genesisInput.cores, + genesisInput.tbaSalt, + genesisInput.tbaImplementation, + genesisInput.daoVotingPeriod, + genesisInput.daoThreshold + ); + + const filter = agentFactory.filters.NewApplication; + const events = await agentFactory.queryFilter(filter, -1); + const event = events[0]; + const { id } = event.args; + return id; + } + + async function deployWithApplication() { + const base = await deployBaseContracts(); + + const { founder } = await getAccounts(); + const id = await createApplication(base, founder, 0); + return { applicationId: id, ...base }; + } + + async function createAgent(base, applicationId) { + const { agentFactory } = base; + await agentFactory.executeApplication(applicationId, true); + + const factoryFilter = agentFactory.filters.NewPersona; + const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); + const factoryEvent = factoryEvents[0]; + return factoryEvent.args; + } + + async function deployWithAgent() { + const base = await deployWithApplication(); + const { applicationId } = base; + + const { founder } = await getAccounts(); + + const { virtualId, token, veToken, dao, tba, lp } = await createAgent( + base, + applicationId + ); + + const veTokenContract = await ethers.getContractAt("AgentVeToken", veToken); + await veTokenContract.connect(founder).delegate(founder.address); // We want to vote instead of letting default delegatee to vote + + return { + ...base, + agent: { + virtualId, + token, + veToken, + dao, + tba, + lp, + }, + }; + } + + async function createContribution( + virtualId, + coreId, + maturity, + parentId, + isModel, + datasetId, + desc, + base, + account, + voters + ) { + const { serviceNft, contributionNft, minter, agentNft } = base; + const daoAddr = (await agentNft.virtualInfo(virtualId)).dao; + const veAddr = (await agentNft.virtualLP(virtualId)).veToken; + const agentDAO = await ethers.getContractAt("AgentDAO", daoAddr); + const veToken = await ethers.getContractAt("AgentVeToken", veAddr); + + const descHash = getDescHash(desc); + + const mintCalldata = await getMintServiceCalldata( + serviceNft, + virtualId, + descHash + ); + + await agentDAO.propose([serviceNft.target], [0], [mintCalldata], desc); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); + const event = events[0]; + const proposalId = event.args[0]; + + await contributionNft.mint( + account, + virtualId, + coreId, + TOKEN_URI, + proposalId, + parentId, + isModel, + datasetId + ); + const voteParams = isModel + ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) + : "0x"; + + for (const voter of voters) { + await agentDAO + .connect(voter) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + } + + await mine(600); + + await agentDAO.execute(proposalId); + await minter.mint(proposalId); + + return proposalId; + } + + function getDescHash(str) { + return ethers.keccak256(ethers.toUtf8Bytes(str)); + } + + before(async function () { + const signers = await ethers.getSigners(); + this.accounts = signers.map((signer) => signer.address); + this.signers = signers; + }); + + + it("should set correct maturity score", async function () { + const { persona, proposalId, service, contribution } = await loadFixture( + proposeContribution + ); + await contribution.mint( + this.accounts[1], + persona.virtualId, + 0, + TOKEN_URI, + proposalId, + "0x0000000000000000000000000000000000000000", + true, + 0 + ); + const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + /* + Scenario: + 1. Validator1 with 100000 votes set maturity score to 1500 + 2. Validator2 with 50000 votes set maturity score to 2000 + 3. Validator3 with 70000 votes set maturity score to 3000 + 4. Maturity = (1500 * 100000 + 2000 * 50000 + 3000 * 50000) / 220000 = 2090.9090 + */ + const [validator1, validator2, validator3] = this.signers; + const personaToken = await ethers.getContractAt( + "AgentToken", + persona.token + ); + expect( + formatEther(await personaToken.getVotes(validator1.address)) + ).to.be.equal("100000.0"); + expect( + formatEther(await personaToken.getVotes(validator2.address)) + ).to.be.equal("50000.0"); + expect( + formatEther(await personaToken.getVotes(validator3.address)) + ).to.be.equal("70000.0"); + const voteParams = abi.encode( + ["uint256", "uint8[] memory"], + [1500, [0, 1, 1, 0, 2]] + ); + const voteParams2 = abi.encode( + ["uint256", "uint8[] memory"], + [2000, [0, 1, 1, 0, 2]] + ); + const voteParams3 = abi.encode( + ["uint256", "uint8[] memory"], + [3000, [0, 1, 1, 0, 2]] + ); + + await expect( + personaDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams) + ) + .to.emit(personaDAO, "ValidatorEloRating") + .withArgs(proposalId, validator1.address, 1500, [0, 1, 1, 0, 2]); + await personaDAO + .connect(validator2) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams2); + await personaDAO + .connect(validator3) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams3); + await mine(43200 * 7); + await personaDAO.execute(proposalId); + expect(await service.getMaturity(proposalId)).to.be.equal(2090n); + expect(await service.getImpact(proposalId)).to.be.equal(2090n); + }); + + it("should show correct impact score", async function () { + const { persona, proposalId, service, contribution } = await loadFixture( + proposeContribution + ); + await contribution.mint( + this.accounts[1], + persona.virtualId, + 0, + TOKEN_URI, + proposalId, + "0x0000000000000000000000000000000000000000", + true, + 0 + ); + const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + /* + Scenario: + Continuing from previous test case, the first service NFT has maturity score of 2090 and we are improving it to 4000, the impact should be 4000-2090 = 1910 + */ + // Proposal 1 + const voteParams = abi.encode( + ["uint256", "uint8[] memory"], + [1500, [0, 1, 1, 0, 2]] + ); + const voteParams2 = abi.encode( + ["uint256", "uint8[] memory"], + [2000, [0, 1, 1, 0, 2]] + ); + const voteParams3 = abi.encode( + ["uint256", "uint8[] memory"], + [3000, [0, 1, 1, 0, 2]] + ); + const voteParams4 = abi.encode( + ["uint256", "uint8[] memory"], + [4000, [0, 1, 1, 0, 2]] + ); + const [validator1, validator2, validator3] = this.signers; + await personaDAO.castVoteWithReasonAndParams( + proposalId, + 1, + "lfg", + voteParams + ); + await personaDAO + .connect(validator2) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams2); + await personaDAO + .connect(validator3) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams3); + await mine(43200 * 7); + await personaDAO.execute(proposalId); + + // Proposal 2 + const descHash = getDescHash(CONTRIBUTION_DESC + " V2"); + + const personaDaoContract = await ethers.getContractFactory("AgentDAO"); + const mintCalldata = await getMintServiceCalldata( + service, + persona.virtualId, + descHash + ); + await personaDaoContract + .attach(persona.dao) + .propose( + [service.target], + [0], + [mintCalldata], + CONTRIBUTION_DESC + " V2" + ); + const filter = personaDaoContract.attach(persona.dao).filters + .ProposalCreated; + const events = await personaDaoContract + .attach(persona.dao) + .queryFilter(filter, -1); + const event = events[0]; + const proposalId2 = event.args[0]; + await contribution.mint( + this.accounts[1], + persona.virtualId, + 0, + TOKEN_URI, + proposalId2, + proposalId, + true, + 0 + ); + + await personaDAO.castVoteWithReasonAndParams( + proposalId2, + 1, + "lfg", + voteParams4 + ); + await personaDAO + .connect(validator2) + .castVoteWithReasonAndParams(proposalId2, 1, "lfg", voteParams4); + await personaDAO + .connect(validator3) + .castVoteWithReasonAndParams(proposalId2, 1, "lfg", voteParams4); + await mine(43200 * 7); + await personaDAO.execute(proposalId2); + + expect(await service.getMaturity(proposalId2)).to.be.equal(4000n); + expect(await service.getImpact(proposalId2)).to.be.equal(1910n); + }); + + it("should allow contribution admin to create proposal", async () => { + const signers = await ethers.getSigners(); + const { persona, proposalId, service, contribution } = await loadFixture( + proposeContribution + ); + const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + await contribution.setAdmin(signers[15].address); + await expect( + personaDAO + .connect(signers[15]) + .propose([personaDAO.target], [0], [ethers.ZeroAddress], "Test1") + ).to.emit(personaDAO, "ProposalCreated"); + + await expect( + personaDAO + .connect(signers[14]) + .propose([personaDAO.target], [0], [ethers.ZeroAddress], "Test2") + ).to.be.reverted; + + await contribution.connect(signers[15]).setAdmin(signers[14].address); + + await expect( + personaDAO + .connect(signers[14]) + .propose([personaDAO.target], [0], [ethers.ZeroAddress], "Test3") + ).to.emit(personaDAO, "ProposalCreated"); + }); + + it("should increase validator score with for,against,abstain", async function () { + const signers = await ethers.getSigners(); + const { persona, proposalId, service, contribution } = await loadFixture( + proposeContribution + ); + const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + + const [validator1, validator2, validator3] = signers; + const voteParams = abi.encode( + ["uint256", "uint8[] memory"], + [20, [0, 1, 1, 0, 2]] + ); + + expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); + expect(await personaDAO.scoreOf(validator2)).to.be.equal(0); + expect(await personaDAO.scoreOf(validator3)).to.be.equal(0); + await personaDAO.castVoteWithReasonAndParams( + proposalId, + 0, + "lfg", + voteParams + ); + await personaDAO + .connect(validator2) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + await personaDAO + .connect(validator3) + .castVoteWithReasonAndParams(proposalId, 2, "lfg", voteParams); + expect(await personaDAO.scoreOf(validator1)).to.be.equal(1n); + expect(await personaDAO.scoreOf(validator2)).to.be.equal(1n); + expect(await personaDAO.scoreOf(validator3)).to.be.equal(1n); + }); + + it("should reject double votes", async function () { + const signers = await ethers.getSigners(); + const { persona, proposalId, service, contribution } = await loadFixture( + proposeContribution + ); + const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + + const [validator1, validator2, validator3] = signers; + const voteParams = abi.encode( + ["uint256", "uint8[] memory"], + [20, [0, 1, 1, 0, 2]] + ); + + await personaDAO.castVoteWithReasonAndParams( + proposalId, + 0, + "lfg", + voteParams + ); + await expect( + personaDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams) + ).to.be.reverted; + }); + + it("should not increase validator score with deliberate votes", async function () { + const signers = await ethers.getSigners(); + const { persona, proposalId, service, contribution } = await loadFixture( + proposeContribution + ); + const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + + const [validator1, validator2, validator3] = signers; + const voteParams = abi.encode( + ["uint256", "uint8[] memory"], + [20, [0, 1, 1, 0, 2]] + ); + + expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); + await personaDAO.castVoteWithReasonAndParams( + proposalId, + 3, + "lfg", + voteParams + ); + expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); + }); + + it("should not increase validator score with deliberate votes after a valid vote", async function () { + const signers = await ethers.getSigners(); + const { persona, proposalId, service, contribution } = await loadFixture( + proposeContribution + ); + const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + + const [validator1, validator2, validator3] = signers; + const voteParams = abi.encode( + ["uint256", "uint8[] memory"], + [20, [0, 1, 1, 0, 2]] + ); + + expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); + await personaDAO.castVoteWithReasonAndParams( + proposalId, + 1, + "lfg", + voteParams + ); + expect(await personaDAO.scoreOf(validator1)).to.be.equal(1n); + await personaDAO.castVoteWithReasonAndParams( + proposalId, + 3, + "lfg", + voteParams + ); + expect(await personaDAO.scoreOf(validator1)).to.be.equal(1n); + }); + + it("should not emit ValidatorEloRating for deliberate votes", async function () { + const signers = await ethers.getSigners(); + const { persona, proposalId, service, contribution } = await loadFixture( + proposeContribution + ); + const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); + + // Only contribution proposal will emit ValidatorEloRating event + await contribution.mint( + this.accounts[1], + persona.virtualId, + 0, + TOKEN_URI, + proposalId, + "0x0000000000000000000000000000000000000000", + true, + 0 + ); + + const voteParams = abi.encode( + ["uint256", "uint8[] memory"], + [20, [0, 1, 1, 0, 2]] + ); + + await expect( + personaDAO.castVoteWithReasonAndParams(proposalId, 3, "lfg", voteParams) + ).to.not.to.emit(personaDAO, "ValidatorEloRating"); + await expect( + personaDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams) + ).to.emit(personaDAO, "ValidatorEloRating"); + }); +}); diff --git a/test/staking.js b/test/deprecated/staking.js similarity index 100% rename from test/staking.js rename to test/deprecated/staking.js diff --git a/test/rewardsV2.js b/test/rewardsV2.js index 32707a2..57267f0 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -119,7 +119,6 @@ describe("RewardsV2", function () { contribution.target, agentNft.target, process.env.IP_SHARES, - process.env.DATA_SHARES, process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, @@ -307,59 +306,6 @@ describe("RewardsV2", function () { before(async function () {}); - it("should mint agent token for successful contribution", async function () { - const base = await loadFixture(deployWithAgent); - const { contributor1, founder } = await getAccounts(); - const maturity = 55; - const agentToken = await ethers.getContractAt( - "AgentToken", - base.agent.token - ); - const balance1 = await agentToken.balanceOf(contributor1.address); - expect(balance1).to.equal(0n); - await createContribution( - 1, - 0, - maturity, - 0, - true, - 0, - "Test", - base, - contributor1.address, - [founder] - ); - const balance2 = await agentToken.balanceOf(contributor1.address); - expect(balance2).to.equal(parseEther(maturity.toString())); - }); - - it("should mint agent token for IP owner on successful contribution", async function () { - const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1, founder } = await getAccounts(); - const maturity = 55; - const agentToken = await ethers.getContractAt( - "AgentToken", - base.agent.token - ); - const balance1 = await agentToken.balanceOf(ipVault.address); - expect(balance1).to.equal(0n); - await createContribution( - 1, - 0, - maturity, - 0, - true, - 0, - "Test", - base, - contributor1.address, - [founder] - ); - - const balance2 = await agentToken.balanceOf(ipVault.address); - expect(balance2).to.equal(parseEther((maturity * 0.1).toString())); - }); - it("should be able to distribute protocol emission for single virtual", async function () { const base = await loadFixture(deployWithAgent); const { rewards, virtualToken, agent } = base; diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index 4e8e386..448c14e 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -99,7 +99,6 @@ describe("AgentFactoryV2", function () { contribution.target, agentNft.target, process.env.IP_SHARES, - process.env.DATA_SHARES, process.env.IMPACT_MULTIPLIER, ipVault.address, agentFactory.target, @@ -284,12 +283,18 @@ describe("AgentFactoryV2", function () { it("agent component C3: Agent veToken", async function () { const { agent } = await loadFixture(deployWithAgent); - const { founder } = await getAccounts(); + const { founder, deployer } = await getAccounts(); const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); const balance = await veToken.balanceOf(founder.address); expect(parseFloat(formatEther(balance).toString()).toFixed(2)).to.be.equal( "100000.00" ); + // Delegate to default delegatee + expect( + parseFloat( + formatEther(await veToken.getVotes(deployer.address)).toString() + ).toFixed(2) + ).to.be.equal("100000.00"); }); it("agent component C4: Agent DAO", async function () { @@ -381,7 +386,7 @@ describe("AgentFactoryV2", function () { capital, [virtualToken.target, agent.token], trader.address, - Math.floor(new Date().getTime() / 1000 + 6000) + Math.floor(new Date().getTime() / 1000 + 600000) ); //// // Start providing liquidity @@ -636,7 +641,7 @@ describe("AgentFactoryV2", function () { ).to.be.revertedWith("Not mature yet"); }); - it("should allow manual unlock LP", async function () { + it("should allow manual unlock staked LP", async function () { const { agent, agentNft, virtualToken } = await loadFixture( deployWithAgent ); @@ -647,11 +652,94 @@ describe("AgentFactoryV2", function () { "AgentVeToken", agent.veToken ); + // Unable to withdraw staked LP initially + await expect(agentVeToken.connect(founder).withdraw(parseEther("10"))).to.be + .reverted; + + // Unlock + await expect(agentVeToken.setMatureAt(0)).to.not.be.reverted; await agentNft.setBlacklist(agent.virtualId, true); expect(await agentNft.isBlacklisted(agent.virtualId)).to.be.equal(true); - await expect(agentVeToken.setMatureAt(0)).to.not.be.reverted; + // Able to withdraw after unlock await expect(agentVeToken.connect(founder).withdraw(parseEther("10"))).to .not.be.reverted; }); + + it("should not allow staking on blacklisted agent", async function () { + const { agent, agentNft, virtualToken } = await loadFixture( + deployWithAgent + ); + const { founder, deployer, trader } = await getAccounts(); + // Assign admin role + await agentNft.grantRole(await agentNft.ADMIN_ROLE(), deployer); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + const agentVeToken = await ethers.getContractAt( + "AgentVeToken", + agent.veToken + ); + // Unable to withdraw staked LP initially + await expect(agentVeToken.connect(founder).withdraw(parseEther("10"))).to.be + .reverted; + + // Unlock + await agentVeToken.setMatureAt(0); + await agentNft.setBlacklist(agent.virtualId, true); + await agentNft.isBlacklisted(agent.virtualId); + + // Unable to stake on blacklisted agent + // Get trader to stake on poorMan so that we have 2 validators + // Buy tokens + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + const amountToBuy = parseEther("90"); + const capital = parseEther("200"); + await virtualToken.mint(trader.address, capital); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + + await router + .connect(trader) + .swapTokensForExactTokens( + amountToBuy, + capital, + [virtualToken.target, agent.token], + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + //// + // Start providing liquidity + const lpToken = await ethers.getContractAt("ERC20", agent.lp); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + await veToken.connect(founder).setCanStake(true); + expect(await lpToken.balanceOf(trader.address)).to.be.equal(0n); + await agentToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, amountToBuy); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, capital); + await router + .connect(trader) + .addLiquidity( + agentToken.target, + virtualToken.target, + await agentToken.balanceOf(trader.address), + await virtualToken.balanceOf(trader.address), + 0, + 0, + trader.address, + Math.floor(new Date().getTime() / 1000 + 6000) + ); + /// + await lpToken.connect(trader).approve(agent.veToken, parseEther("10")); + await expect( + veToken + .connect(trader) + .stake(parseEther("10"), trader.address, trader.address) + ).to.be.revertedWith("Agent Blacklisted"); + }); }); From bb89402e90789af4c3387d22d0baa4f4cd3d4172 Mon Sep 17 00:00:00 2001 From: kw Date: Thu, 13 Jun 2024 23:17:37 +0800 Subject: [PATCH 27/30] updated minter --- contracts/token/Minter.sol | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol index 364fe5e..d342bc9 100644 --- a/contracts/token/Minter.sol +++ b/contracts/token/Minter.sol @@ -102,7 +102,10 @@ contract Minter is IMinter, Ownable { emit ImpactMultiplierUpdated(_multiplier); } - function setImpactMulOverride(uint256 virtualId, uint256 mul) public onlyAgentDAO(virtualId) { + function setImpactMulOverride( + uint256 virtualId, + uint256 mul + ) public onlyAgentDAO(virtualId) { impactMulOverrides[virtualId] = mul; emit AgentImpactMultiplierUpdated(virtualId, mul); } @@ -147,19 +150,6 @@ contract Minter is IMinter, Ownable { 10 ** 18) / DENOM : 0; uint256 ipAmount = ((amount + dataAmount) * ipShare) / DENOM; - address tokenAddress = IAgentNft(agentNft).virtualInfo(agentId).token; - IContributionNft contribution = IContributionNft(contributionNft); - require(contribution.isModel(nftId), "Not a model contribution"); - - uint256 datasetId = contribution.getDatasetId(nftId); - uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * impactMultiplier * 10 ** 18) / DENOM; - uint256 ipAmount = (amount * ipShare) / DENOM; - uint256 dataAmount = 0; - - if (datasetId != 0) { - dataAmount = (amount * dataShare) / DENOM; - amount = amount - dataAmount; - } // Mint to model owner if (amount > 0) { From 7c2d5f10b9c90bfeb74ef2c6e2b262cb4f972887 Mon Sep 17 00:00:00 2001 From: kahwaipd <100838692+kahwaipd@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:56:11 +0800 Subject: [PATCH 28/30] Feat/auto execute (#27) * feat: modified testing to guarantee output amounts (#26) * auto execute proposal when forVotes == totalSupply * updated delegate test cases to match v2 changes * agentDAO proposals will be auto executed --------- Co-authored-by: twx-pathdao <97669360+twx-pathdao@users.noreply.github.com> --- contracts/virtualPersona/AgentDAO.sol | 14 ++ test/agentDAO.js | 71 ++------ test/contribution.js | 4 - test/delegate.js | 233 ++++++++++++++------------ test/rewardsV2.js | 64 +++++-- 5 files changed, 205 insertions(+), 181 deletions(-) diff --git a/contracts/virtualPersona/AgentDAO.sol b/contracts/virtualPersona/AgentDAO.sol index 90fa679..1b86ee5 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -176,9 +176,23 @@ contract AgentDAO is } } + if (support == 1) { + _tryAutoExecute(proposalId); + } + return weight; } + // Auto execute when forVotes == totalSupply + function _tryAutoExecute(uint256 proposalId) internal { + (, uint256 forVotes, ) = proposalVotes(proposalId); + if ( + forVotes == token().getPastTotalSupply(proposalSnapshot(proposalId)) + ) { + execute(proposalId); + } + } + function _updateMaturity( address account, uint256 proposalId, diff --git a/test/agentDAO.js b/test/agentDAO.js index 09c491f..72cbf89 100644 --- a/test/agentDAO.js +++ b/test/agentDAO.js @@ -17,7 +17,7 @@ const { parseEther, formatEther } = require("ethers"); const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, - false + false, ]); }; @@ -197,7 +197,9 @@ describe("AgentDAO", function () { const { agentFactory, applicationId } = base; const { founder } = await getAccounts(); - await agentFactory.connect(founder).executeApplication(applicationId, false); + await agentFactory + .connect(founder) + .executeApplication(applicationId, false); const factoryFilter = agentFactory.filters.NewPersona; const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); @@ -218,62 +220,9 @@ describe("AgentDAO", function () { }; } - async function createContribution( - coreId, - maturity, - parentId, - isModel, - datasetId, - desc, - base, - account - ) { - const { founder } = await getAccounts(); - const { agent, serviceNft, contributionNft, minter } = base; - const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); - - const descHash = getDescHash(desc); - - const mintCalldata = await getMintServiceCalldata( - serviceNft, - agent.virtualId, - descHash - ); - - await agentDAO.propose([serviceNft.target], [0], [mintCalldata], desc); - const filter = agentDAO.filters.ProposalCreated; - const events = await agentDAO.queryFilter(filter, -1); - const event = events[0]; - const proposalId = event.args[0]; - - await contributionNft.mint( - account, - agent.virtualId, - coreId, - TOKEN_URI, - proposalId, - parentId, - isModel, - datasetId - ); - - const voteParams = isModel - ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) - : "0x"; - await agentDAO - .connect(founder) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); - await mine(600); - - await agentDAO.execute(proposalId); - await minter.mint(proposalId); - - return proposalId; - } - before(async function () {}); - it("should allow early execution when forVotes == totalSupply", async function () { + it("should auto early execution when forVotes == totalSupply", async function () { const base = await loadFixture(deployWithAgent); const { founder, deployer } = await getAccounts(); const { @@ -300,11 +249,17 @@ describe("AgentDAO", function () { const event = events[0]; const proposalId = event.args[0]; + // Proposal not executed yet + await expect(serviceNft.ownerOf(proposalId)).to.be.reverted; + await mine(10); await agentDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", "0x"); + + // Proposal should be auto executed + expect(await serviceNft.ownerOf(proposalId)).to.equal(agent.tba); const state = await agentDAO.state(proposalId); - expect(state).to.equal(4n); - await expect(agentDAO.execute(proposalId)).to.not.rejected; + expect(state).to.equal(7n); + await expect(agentDAO.execute(proposalId)).to.be.reverted; }); it("should not allow early execution when forVotes < totalSupply although met quorum", async function () { diff --git a/test/contribution.js b/test/contribution.js index b844d3a..be55a00 100644 --- a/test/contribution.js +++ b/test/contribution.js @@ -306,7 +306,6 @@ describe("Contribution", function () { await mine(600); - await agentDAO.execute(proposalId); if (isModel) { await minter.mint(proposalId); } @@ -546,7 +545,6 @@ describe("Contribution", function () { await mine(1); await agentDAO.connect(founder).castVote(proposalId, 1); await mine(1); - await agentDAO.execute(proposalId); // No agent token minted for dataset contribution const c1 = await createContribution( @@ -693,8 +691,6 @@ describe("Contribution", function () { await mine(1); await agentDAO.connect(founder).castVote(proposalId, 1); - await mine(1); - await agentDAO.execute(proposalId); // No agent token minted for dataset contribution const c1 = await createContribution( diff --git a/test/delegate.js b/test/delegate.js index 478de5f..2944abc 100644 --- a/test/delegate.js +++ b/test/delegate.js @@ -11,7 +11,7 @@ const { const getExecuteCallData = (factory, proposalId) => { return factory.interface.encodeFunctionData("executeApplication", [ proposalId, - false + false, ]); }; @@ -34,131 +34,150 @@ describe("Delegation", function () { daoThreshold: 1000000000000000000000n, }; - async function deployBaseContracts() { - const [deployer] = await ethers.getSigners(); - const veToken = await ethers.deployContract( - "veVirtualToken", - [deployer.address], - {} - ); - await veToken.waitForDeployment(); + const getAccounts = async () => { + const [deployer, ipVault, founder, poorMan, trader, treasury] = + await ethers.getSigners(); + return { deployer, ipVault, founder, poorMan, trader, treasury }; + }; - const demoToken = await ethers.deployContract( - "BMWToken", - [deployer.address], - {} - ); - await demoToken.waitForDeployment(); + async function deployBaseContracts() { + const { deployer, ipVault, treasury } = await getAccounts(); - const protocolDAO = await ethers.deployContract( - "VirtualProtocolDAO", - [veToken.target, 0, PROTOCOL_DAO_VOTING_PERIOD, PROPOSAL_THRESHOLD, 500], + const virtualToken = await ethers.deployContract( + "VirtualToken", + [PROPOSAL_THRESHOLD, deployer.address], {} ); - await protocolDAO.waitForDeployment(); + await virtualToken.waitForDeployment(); - const AgentNft = await ethers.getContractFactory("AgentNft"); - const personaNft = await upgrades.deployProxy(AgentNft, [deployer.address]); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); + const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); const contribution = await upgrades.deployProxy( await ethers.getContractFactory("ContributionNft"), - [personaNft.target], + [agentNft.target], {} ); const service = await upgrades.deployProxy( await ethers.getContractFactory("ServiceNft"), - [personaNft.target, contribution.target, 7000n], + [agentNft.target, contribution.target, process.env.DATASET_SHARES], {} ); - await personaNft.setContributionService( - contribution.target, - service.target - ); + await agentNft.setContributionService(contribution.target, service.target); - const personaToken = await ethers.deployContract("AgentToken"); - await personaToken.waitForDeployment(); - const personaDAO = await ethers.deployContract("AgentDAO"); - await personaDAO.waitForDeployment(); + // Implementation contracts + const agentToken = await ethers.deployContract("AgentToken"); + await agentToken.waitForDeployment(); + const agentDAO = await ethers.deployContract("AgentDAO"); + await agentDAO.waitForDeployment(); + const agentVeToken = await ethers.deployContract("AgentVeToken"); + await agentVeToken.waitForDeployment(); - const tba = await ethers.deployContract("ERC6551Registry"); - - const personaFactory = await upgrades.deployProxy( - await ethers.getContractFactory("AgentFactory"), + const agentFactory = await upgrades.deployProxy( + await ethers.getContractFactory("AgentFactoryV2"), [ - personaToken.target, - personaDAO.target, - tba.target, - demoToken.target, - personaNft.target, - parseEther("100000"), - 5, - protocolDAO.target, + agentToken.target, + agentVeToken.target, + agentDAO.target, + process.env.TBA_REGISTRY, + virtualToken.target, + agentNft.target, + PROPOSAL_THRESHOLD, deployer.address, ] ); - - await personaNft.grantRole( - await personaNft.MINTER_ROLE(), - personaFactory.target + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + const minter = await ethers.deployContract("Minter", [ + service.target, + contribution.target, + agentNft.target, + process.env.IP_SHARES, + process.env.IMPACT_MULTIPLIER, + ipVault.address, + agentFactory.target, + deployer.address, + ]); + await minter.waitForDeployment(); + await agentFactory.setMinter(minter.target); + await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years + await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); + await agentFactory.setTokenAdmin(deployer.address); + await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION ); - - await demoToken.mint(deployer.address, PROPOSAL_THRESHOLD); - await demoToken.approve(personaFactory.target, PROPOSAL_THRESHOLD); - - await personaFactory.proposePersona( - genesisInput.name, - genesisInput.symbol, - genesisInput.tokenURI, - genesisInput.cores, - genesisInput.tbaSalt, - genesisInput.tbaImplementation, - genesisInput.daoVotingPeriod, - genesisInput.daoThreshold + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address ); + await agentFactory.setDefaultDelegatee(deployer.address); + + return { virtualToken, agentFactory, agentNft }; + } - const filter = personaFactory.filters.NewApplication; - const events = await personaFactory.queryFilter(filter, -1); + async function deployWithApplication() { + const base = await deployBaseContracts(); + const { agentFactory, virtualToken } = base; + const { founder } = await getAccounts(); + + // Prepare tokens for proposal + await virtualToken.mint(founder.address, PROPOSAL_THRESHOLD); + await virtualToken + .connect(founder) + .approve(agentFactory.target, PROPOSAL_THRESHOLD); + + const tx = await agentFactory + .connect(founder) + .proposeAgent( + genesisInput.name, + genesisInput.symbol, + genesisInput.tokenURI, + genesisInput.cores, + genesisInput.tbaSalt, + genesisInput.tbaImplementation, + genesisInput.daoVotingPeriod, + genesisInput.daoThreshold + ); + + const filter = agentFactory.filters.NewApplication; + const events = await agentFactory.queryFilter(filter, -1); const event = events[0]; const { id } = event.args; + return { applicationId: id, ...base }; + } - // Create proposal - await veToken.oracleTransfer( - [ethers.ZeroAddress], - [deployer.address], - [parseEther("100000000")] - ); - await veToken.delegate(deployer.address); - - await protocolDAO.propose( - [personaFactory.target], - [0], - [getExecuteCallData(personaFactory, id)], - "Create Jessica" - ); - - const daoFilter = protocolDAO.filters.ProposalCreated; - const daoEvents = await protocolDAO.queryFilter(daoFilter, -1); - const daoEvent = daoEvents[0]; - const daoProposalId = daoEvent.args[0]; + async function deployWithAgent() { + const base = await deployWithApplication(); + const { agentFactory, applicationId } = base; - await protocolDAO.castVote(daoProposalId, 1); - await mine(PROTOCOL_DAO_VOTING_PERIOD); + const { founder } = await getAccounts(); + await agentFactory + .connect(founder) + .executeApplication(applicationId, false); - await protocolDAO.execute(daoProposalId); - const factoryFilter = personaFactory.filters.NewPersona; - const factoryEvents = await personaFactory.queryFilter(factoryFilter, -1); + const factoryFilter = agentFactory.filters.NewPersona; + const factoryEvents = await agentFactory.queryFilter(factoryFilter, -1); const factoryEvent = factoryEvents[0]; - const { virtualId, token, dao } = factoryEvent.args; - const persona = { virtualId, token, dao }; - - const personaTokenContract = await ethers.getContractAt( - "AgentToken", - persona.token - ); - return { personaTokenContract }; + const { virtualId, token, veToken, dao, tba, lp } = await factoryEvent.args; + + return { + ...base, + agent: { + virtualId, + token, + veToken, + dao, + tba, + lp, + }, + }; } before(async function () { @@ -168,46 +187,48 @@ describe("Delegation", function () { }); it("should be able to retrieve past delegates", async function () { - const { personaTokenContract } = await loadFixture(deployBaseContracts); + const { agent } = await loadFixture(deployWithAgent); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); const [account1, account2, account3] = this.accounts; - await personaTokenContract.delegate(account1); + await veToken.delegate(account1); mine(1); const block1 = await ethers.provider.getBlockNumber(); - expect(await personaTokenContract.delegates(account1)).to.equal(account1); + expect(await veToken.delegates(account1)).to.equal(account1); - await personaTokenContract.delegate(account2); + await veToken.delegate(account2); mine(1); const block2 = await ethers.provider.getBlockNumber(); - await personaTokenContract.delegate(account3); + await veToken.delegate(account3); mine(1); const block3 = await ethers.provider.getBlockNumber(); expect( - await personaTokenContract.getPastDelegates(account1, block2) + await veToken.getPastDelegates(account1, block2) ).to.equal(account2); expect( - await personaTokenContract.getPastDelegates(account1, block3) + await veToken.getPastDelegates(account1, block3) ).to.equal(account3); expect( - await personaTokenContract.getPastDelegates(account1, block1) + await veToken.getPastDelegates(account1, block1) ).to.equal(account1); - expect(await personaTokenContract.delegates(account1)).to.equal(account3); + expect(await veToken.delegates(account1)).to.equal(account3); }); it("should be able to retrieve past delegates when there are more than 5 checkpoints", async function () { - const { personaTokenContract } = await loadFixture(deployBaseContracts); + const { agent } = await loadFixture(deployWithAgent); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); const blockNumber = await ethers.provider.getBlockNumber(); const [account1, account2, account3] = this.accounts; for (let i = 0; i < 8; i++) { - await personaTokenContract.delegate(this.accounts[i]); + await veToken.delegate(this.accounts[i]); } await mine(1); for (let i = 0; i < 8; i++) { expect( - await personaTokenContract.getPastDelegates( + await veToken.getPastDelegates( account1, blockNumber + i + 1 ) diff --git a/test/rewardsV2.js b/test/rewardsV2.js index a5feadc..3f13ca8 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -298,7 +298,6 @@ describe("RewardsV2", function () { await mine(600); - await agentDAO.execute(proposalId); await minter.mint(proposalId); return proposalId; @@ -1006,7 +1005,9 @@ describe("RewardsV2", function () { const app3 = await createApplication(base, founder, 2); const agent3 = await createAgent(base, app3); const app4 = await createApplication(base, founder, 3); - const agent4 = await createAgent(base, app4); + const agent4 = await createAgent(base, app4); + const lp3 = await rewards.getLPValue(agent3[0]); + console.log("Initial LP ", lp3); // Create contributions for all 4 virtuals for (let i = 1; i <= 4; i++) { @@ -1027,7 +1028,7 @@ describe("RewardsV2", function () { 0, `Test ${i}`, base, - contributor1.address, + trader.address, [validator1] ); } @@ -1036,32 +1037,53 @@ describe("RewardsV2", function () { "IUniswapV2Router02", process.env.UNISWAP_ROUTER ); - // Trade on different LP - await virtualToken.mint(trader.address, parseEther("300")); + // Add liquidity of 0.25, 0.75 and 1 + await virtualToken.mint(trader.address, parseEther("400")); await virtualToken .connect(trader) - .approve(router.target, parseEther("300")); + .approve(router.target, parseEther("400")); for (let i of [1, 3, 4]) { const agentTokenAddr = (await agentNft.virtualInfo(i)).token; - const amountToBuy = parseEther((20 * i).toString()); - const capital = parseEther("100"); + const agentToken = await ethers.getContractAt("AgentToken", agentTokenAddr); + await agentToken.connect(trader).approve(router.target, parseEther("400")) + const amountToAdd = parseEther((0.25 * i).toString()); + const capital = parseEther("400"); await router .connect(trader) - .swapTokensForExactTokens( - amountToBuy, - capital, - [virtualToken.target, agentTokenAddr], + .addLiquidity( + virtualToken.target, + agentTokenAddr, + amountToAdd, + amountToAdd, + amountToAdd, + amountToAdd, trader.address, Math.floor(new Date().getTime() / 1000 + 6000000) ); + const lp_after_buy = await rewards.getLPValue(i); + console.log("lp after buy LP ", lp_after_buy); await mine(1); } // Distribute rewards // Expectations: - // virtual 4>3>1 + // virtual 4 = 100001000000000000000000n, + // virtual 3 = 100000750000000000000000n, + // virtual 1 = 100000250000000000000000n // virtual 2 = 0 + + // Using gwei as calculation size due to BigInt limitations const rewardSize = 300000; + const Lp4 = BigInt(100001000000000); + const Lp3 = BigInt(100000750000000); + const Lp1 = BigInt(100000250000000); + const rewardSizeInGwei = BigInt(rewardSize * 10 ** 9) + + + const totalLp = Lp1 + Lp3 + Lp4 + console.log("totallp", totalLp) + + await virtualToken.approve( rewards.target, parseEther(rewardSize.toString()) @@ -1076,6 +1098,11 @@ describe("RewardsV2", function () { founder.address, [1] ); + + //99999583336111092592716 + const rewardSizeAfterProtocol = rewardSizeInGwei * BigInt(process.env.STAKER_SHARES) / BigInt(10000) + const expectedRewardLP1Ratio = await (rewardSizeAfterProtocol * Lp1) / totalLp + const rewards2 = await rewards.getTotalClaimableStakerRewards( founder.address, [2] @@ -1084,10 +1111,21 @@ describe("RewardsV2", function () { founder.address, [3] ); + const expectedRewardLP3Ratio = await (rewardSizeAfterProtocol * Lp3) / totalLp + + const rewards4 = await rewards.getTotalClaimableStakerRewards( founder.address, [4] ); + const expectedRewardLP4Ratio = (rewardSizeAfterProtocol * Lp4) / totalLp + + + expect(await parseInt(ethers.formatUnits(rewards1.toString(), "gwei"))).to.be.equal(expectedRewardLP1Ratio); + expect(await parseInt(ethers.formatUnits(rewards3.toString(), "gwei"))).to.be.equal(expectedRewardLP3Ratio); + expect(await parseInt(ethers.formatUnits(rewards4.toString(), "gwei"))).to.be.equal(expectedRewardLP4Ratio); + + expect(rewards4).to.be.greaterThan(rewards3); expect(rewards3).to.be.greaterThan(rewards1); expect(rewards2).to.be.equal(0n); From 40b754f775b00174ba6f39a5b3a8991619ab3f72 Mon Sep 17 00:00:00 2001 From: kahwaipd <100838692+kahwaipd@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:58:49 +0800 Subject: [PATCH 29/30] Feat/v2 changes (#28) * removed default delegatee * updated agentToken distribution logic * added on-chain elo rating * updated DAO * adjusted test cases to be compatible with latest change requests * removed deprecated test cases * wip * wip * upgraded deploy script --- .openzeppelin/base-sepolia.json | 1092 +++++++++++++++++ contracts/contribution/ContributionNft.sol | 15 +- contracts/contribution/IContributionNft.sol | 1 + contracts/libs/Elo.sol | 70 ++ contracts/libs/FixedPointMathLib.sol | 255 ++++ contracts/token/Minter.sol | 79 +- contracts/virtualPersona/AgentDAO.sol | 49 +- contracts/virtualPersona/AgentFactory.sol | 98 +- contracts/virtualPersona/AgentNftV2.sol | 16 +- contracts/virtualPersona/AgentToken.sol | 42 +- contracts/virtualPersona/EloCalculator.sol | 73 ++ contracts/virtualPersona/IAgentDAO.sol | 2 +- contracts/virtualPersona/IAgentFactory.sol | 2 - contracts/virtualPersona/IAgentNft.sol | 2 + contracts/virtualPersona/IAgentToken.sol | 13 +- contracts/virtualPersona/IERC20Config.sol | 4 + contracts/virtualPersona/IEloCalculator.sol | 9 + scripts/arguments/elo.js | 1 + scripts/arguments/minter.js | 3 +- scripts/v2/deployElo.ts | 20 + scripts/v2/deployMinter.ts | 20 + scripts/v2/migrateV2.ts | 36 +- test/ProtocolDAO.js | 2 +- test/agentDAO.js | 70 +- test/contribution.js | 418 ++++--- test/delegate.js | 40 +- .../{contribution.js => contribution.js.old} | 0 .../{genesisDAO.js => genesisDAO.js.old} | 0 .../deprecated/{rewards.js => rewards.js.old} | 0 .../deprecated/{staking.js => staking.js.old} | 0 test/elo.js | 22 + test/rewardsV2.js | 326 ++--- test/virtualGenesis.js | 108 +- 33 files changed, 2299 insertions(+), 589 deletions(-) create mode 100644 contracts/libs/Elo.sol create mode 100644 contracts/libs/FixedPointMathLib.sol create mode 100644 contracts/virtualPersona/EloCalculator.sol create mode 100644 contracts/virtualPersona/IEloCalculator.sol create mode 100644 scripts/arguments/elo.js create mode 100644 scripts/v2/deployElo.ts create mode 100644 scripts/v2/deployMinter.ts rename test/deprecated/{contribution.js => contribution.js.old} (100%) rename test/deprecated/{genesisDAO.js => genesisDAO.js.old} (100%) rename test/deprecated/{rewards.js => rewards.js.old} (100%) rename test/deprecated/{staking.js => staking.js.old} (100%) create mode 100644 test/elo.js diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json index e26f1bb..387d174 100644 --- a/.openzeppelin/base-sepolia.json +++ b/.openzeppelin/base-sepolia.json @@ -105,6 +105,21 @@ "address": "0xb997e2f88B187Cd5132c5804F5c800d40f85a16B", "txHash": "0xd17f5d2714193a972656e1b000e37740819e8a60bb717cb1c1feba39e5a5b935", "kind": "transparent" + }, + { + "address": "0x5A4f5224CbFD52164f5fB8981fE515a22a0B3f60", + "txHash": "0x18a8a77f56efd0af390c4dad16c35bc145e769091855e727d90c4e2e5a4b3636", + "kind": "transparent" + }, + { + "address": "0x440Ab85200ee3051462631A49a250943A1dA8520", + "txHash": "0xeacab5c53eea2d55601fae46c4cc75a17bff3d4687ff96f623ded8a829ba5b28", + "kind": "transparent" + }, + { + "address": "0x1515e431d79682fA8bB31216c9747FF45FAd26b1", + "txHash": "0x334f81421163340a1fd93e6c2b6cb1b2d2307386354c0b06f6452505795347cc", + "kind": "transparent" } ], "impls": { @@ -10470,6 +10485,1083 @@ "0x7C3454Cb983Ed1D060A4677C02e1126C4a2275B3", "0x8f767259867Cc93df37e59ba445019418871ea57" ] + }, + "9b9eba25e62b1f13b3d13a806b1def57a01724f9f760b0f254fc6a5225f87bf1": { + "address": "0xAA7E2dC2CA07F805dE90b0e638738d7DD34ca388", + "txHash": "0x3ecd97fa81fb951f67330ba32a0f50acab139243849f355767254ee356e2cb5e", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "serviceNft", + "offset": 0, + "slot": "0", + "type": "t_address", + "contract": "Minter", + "src": "contracts/token/Minter.sol:18" + }, + { + "label": "contributionNft", + "offset": 0, + "slot": "1", + "type": "t_address", + "contract": "Minter", + "src": "contracts/token/Minter.sol:19" + }, + { + "label": "agentNft", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "Minter", + "src": "contracts/token/Minter.sol:20" + }, + { + "label": "ipVault", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "Minter", + "src": "contracts/token/Minter.sol:21" + }, + { + "label": "ipShare", + "offset": 0, + "slot": "4", + "type": "t_uint256", + "contract": "Minter", + "src": "contracts/token/Minter.sol:23" + }, + { + "label": "impactMultiplier", + "offset": 0, + "slot": "5", + "type": "t_uint256", + "contract": "Minter", + "src": "contracts/token/Minter.sol:24" + }, + { + "label": "maxImpact", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "Minter", + "src": "contracts/token/Minter.sol:26" + }, + { + "label": "_mintedNfts", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Minter", + "src": "contracts/token/Minter.sol:30" + }, + { + "label": "impactMulOverrides", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "Minter", + "src": "contracts/token/Minter.sol:32" + }, + { + "label": "ipShareOverrides", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "Minter", + "src": "contracts/token/Minter.sol:33" + }, + { + "label": "locked", + "offset": 0, + "slot": "10", + "type": "t_bool", + "contract": "Minter", + "src": "contracts/token/Minter.sol:35" + }, + { + "label": "agentFactory", + "offset": 1, + "slot": "10", + "type": "t_address", + "contract": "Minter", + "src": "contracts/token/Minter.sol:56" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)64_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)14_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "45974d00413b47b6b2928ea43d901e4bddbc4725d942b62a38adc56c534300ce": { + "address": "0x376E1a40b03e1A7c75c6f381C3b9B6249270a42c", + "txHash": "0xc77df46b46c782355ba595f65f92aca3e4ed8a9d4ef423bcb96f2fe599e998da", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "k", + "offset": 0, + "slot": "0", + "type": "t_uint256", + "contract": "EloCalculator", + "src": "contracts/virtualPersona/EloCalculator.sol:11" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)744_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)97_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "c3e1a32d88793554b8f60a8684ead89342981cd7aa39742798449806638da580": { + "address": "0x80377299Da5eED0C991193dB1a0F28A40AE4607C", + "txHash": "0x2ef03f2e0026f33587632f52e7a80695d313a079f702bb8f29c17996e847e612", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)7716_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextAgentRewardId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:32" + }, + { + "label": "rewardToken", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:39" + }, + { + "label": "agentNft", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:40" + }, + { + "label": "_rewards", + "offset": 0, + "slot": "4", + "type": "t_array(t_struct(Reward)19774_storage)dyn_storage", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:43" + }, + { + "label": "_agentRewards", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_uint256,t_array(t_struct(AgentReward)19787_storage)dyn_storage)", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:44" + }, + { + "label": "_rewardSettings", + "offset": 0, + "slot": "6", + "type": "t_struct(Trace)21786_storage", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:46" + }, + { + "label": "protocolRewards", + "offset": 0, + "slot": "7", + "type": "t_uint256", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:49" + }, + { + "label": "locked", + "offset": 0, + "slot": "8", + "type": "t_bool", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:58" + }, + { + "label": "_stakerClaims", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_struct(Claim)19792_storage))", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:233" + }, + { + "label": "_validatorClaims", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_struct(Claim)19792_storage))", + "contract": "AgentRewardV2", + "src": "contracts/AgentRewardV2.sol:234" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)744_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(AgentReward)19787_storage)dyn_storage": { + "label": "struct IAgentReward.AgentReward[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(Checkpoint)21797_storage)dyn_storage": { + "label": "struct RewardSettingsCheckpoints.Checkpoint[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(Reward)19774_storage)dyn_storage": { + "label": "struct IAgentReward.Reward[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)dyn_storage": { + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_struct(Claim)19792_storage))": { + "label": "mapping(address => mapping(uint256 => struct IAgentReward.Claim))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)7716_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_struct(AgentReward)19787_storage)dyn_storage)": { + "label": "mapping(uint256 => struct IAgentReward.AgentReward[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Claim)19792_storage)": { + "label": "mapping(uint256 => struct IAgentReward.Claim)", + "numberOfBytes": "32" + }, + "t_struct(AgentReward)19787_storage": { + "label": "struct IAgentReward.AgentReward", + "members": [ + { + "label": "id", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "rewardIndex", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "stakerAmount", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "validatorAmount", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "totalProposals", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "totalStaked", + "type": "t_uint256", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_struct(Checkpoint)21797_storage": { + "label": "struct RewardSettingsCheckpoints.Checkpoint", + "members": [ + { + "label": "_key", + "type": "t_uint32", + "offset": 0, + "slot": "0" + }, + { + "label": "_value", + "type": "t_struct(RewardSettings)21791_storage", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Claim)19792_storage": { + "label": "struct IAgentReward.Claim", + "members": [ + { + "label": "totalClaimed", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "rewardCount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Reward)19774_storage": { + "label": "struct IAgentReward.Reward", + "members": [ + { + "label": "blockNumber", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lpValues", + "type": "t_array(t_uint256)dyn_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "virtualIds", + "type": "t_array(t_uint256)dyn_storage", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(RewardSettings)21791_storage": { + "label": "struct RewardSettingsCheckpoints.RewardSettings", + "members": [ + { + "label": "protocolShares", + "type": "t_uint16", + "offset": 0, + "slot": "0" + }, + { + "label": "stakerShares", + "type": "t_uint16", + "offset": 2, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)7716_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Trace)21786_storage": { + "label": "struct RewardSettingsCheckpoints.Trace", + "members": [ + { + "label": "_checkpoints", + "type": "t_array(t_struct(Checkpoint)21797_storage)dyn_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "77ae80694b5e37672944d775cf9dd2ca85803b333c7a2d680adfd2423fa243b1": { + "address": "0x3066313E2F8368e980e60a81373143b8387bBf51", + "txHash": "0xfdca336c60ec98ba8fb83dce11f1d2c3a91f0c1bcfa7b8b05edb2ecf6a1b80f0", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)497_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:27" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:28" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:29" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:30" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:31" + }, + { + "label": "applicationThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:32" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:34" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "8", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:35" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:37" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "10", + "type": "t_uint256", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:38" + }, + { + "label": "_applications", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_struct(Application)2388_storage)", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:74" + }, + { + "label": "gov", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:76" + }, + { + "label": "_vault", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:87" + }, + { + "label": "locked", + "offset": 20, + "slot": "13", + "type": "t_bool", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:89" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "14", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:101" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:102" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:103" + }, + { + "label": "_minter", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:104" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:105" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "19", + "type": "t_address", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:106" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:109" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "21", + "type": "t_bytes_storage", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:110" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "22", + "type": "t_uint16", + "contract": "AgentFactoryV2", + "src": "contracts/virtualPersona/AgentFactory.sol:111" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)10_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(PausableStorage)82_storage": { + "label": "struct PausableUpgradeable.PausableStorage", + "members": [ + { + "label": "_paused", + "type": "t_bool", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)2359": { + "label": "enum AgentFactoryV2.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)497_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)2388_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV2.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)2388_storage": { + "label": "struct AgentFactoryV2.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)2359", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(RoleData)497_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Pausable": [ + { + "contract": "PausableUpgradeable", + "label": "_paused", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol:21", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } } } } diff --git a/contracts/contribution/ContributionNft.sol b/contracts/contribution/ContributionNft.sol index 78f2089..c96cb55 100644 --- a/contracts/contribution/ContributionNft.sol +++ b/contracts/contribution/ContributionNft.sol @@ -37,6 +37,8 @@ contract ContributionNft is address private _admin; // Admin is able to create contribution proposal without votes + address private _eloCalculator; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -180,9 +182,16 @@ contract ContributionNft is return _ownerOf(tokenId); } - function getDatasetId( - uint256 tokenId - ) external view returns (uint256) { + function getDatasetId(uint256 tokenId) external view returns (uint256) { return modelDatasets[tokenId]; } + + function getEloCalculator() external view returns (address) { + return _eloCalculator; + } + + function setEloCalculator(address eloCalculator_) public { + require(_msgSender() == _admin, "Only admin can set elo calculator"); + _eloCalculator = eloCalculator_; + } } diff --git a/contracts/contribution/IContributionNft.sol b/contracts/contribution/IContributionNft.sol index 635d43f..4741765 100644 --- a/contracts/contribution/IContributionNft.sol +++ b/contracts/contribution/IContributionNft.sol @@ -13,4 +13,5 @@ interface IContributionNft { function getAdmin() external view returns (address); function getDatasetId(uint256 tokenId) external view returns (uint256); function getAgentDAO(uint256 virtualId) external view returns (IGovernor); + function getEloCalculator() external view returns (address); } diff --git a/contracts/libs/Elo.sol b/contracts/libs/Elo.sol new file mode 100644 index 0000000..0f73e14 --- /dev/null +++ b/contracts/libs/Elo.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {FixedPointMathLib as fp} from "./FixedPointMathLib.sol"; + +library Elo { + /// @notice Get the 16th root of a number, used in ELO calculations + /// @dev Elo calculations require the 400th root (10 ^ (x / 400)), however this can be simplified to the 16th root (10 ^ ((x / 25) / 16)) + function sixteenthRoot(uint256 x) internal pure returns (uint256) { + return fp.sqrt(fp.sqrt(fp.sqrt(fp.sqrt(x)))); + } + + /// @notice Calculates the change in ELO rating, after a given outcome. + /// @param ratingA the ELO rating of the player A + /// @param ratingB the ELO rating of the player B + /// @param score the score of the player A, scaled by 100. 100 = win, 50 = draw, 0 = loss + /// @param kFactor the k-factor or development multiplier used to calculate the change in ELO rating. 20 is the typical value + /// @return change the change in ELO rating of player A, with 2 decimals of precision. 1501 = 15.01 ELO change + /// @return negative the directional change of player A's ELO. Opposite sign for player B + function ratingChange(uint256 ratingA, uint256 ratingB, uint256 score, uint256 kFactor) + internal + pure + returns (uint256 change, bool negative) + { + uint256 _kFactor; // scaled up `kFactor` by 100 + bool _negative = ratingB < ratingA; + uint256 ratingDiff; // absolute value difference between `ratingA` and `ratingB` + + unchecked { + // scale up the inputs by a factor of 100 + // since our elo math is scaled up by 100 (to avoid low precision integer division) + _kFactor = kFactor * 10_000; + ratingDiff = _negative ? ratingA - ratingB : ratingB - ratingA; + } + + // checks against overflow/underflow, discovered via fuzzing + // large rating diffs leads to 10^ratingDiff being too large to fit in a uint256 + require(ratingDiff < 1126, "Rating difference too large"); + // large rating diffs when applying the scale factor leads to underflow (800 - ratingDiff) + if (_negative) require(ratingDiff < 800, "Rating difference too large"); + + // ---------------------------------------------------------------------- + // Below, we'll be running simplified versions of the following formulas: + // expected score = 1 / (1 + 10 ^ (ratingDiff / 400)) + // elo change = kFactor * (score - expectedScore) + + uint256 n; // numerator of the power, with scaling, (numerator of `ratingDiff / 400`) + uint256 _powered; // the value of 10 ^ numerator + uint256 powered; // the value of 16th root of 10 ^ numerator (fully resolved 10 ^ (ratingDiff / 400)) + uint256 kExpectedScore; // the expected score with K factor distributed + uint256 kScore; // the actual score with K factor distributed + + unchecked { + // apply offset of 800 to scale the result by 100 + n = _negative ? 800 - ratingDiff : 800 + ratingDiff; + + // (x / 400) is the same as ((x / 25) / 16)) + _powered = fp.rpow(10, n / 25, 1); // divide by 25 to avoid reach uint256 max + powered = sixteenthRoot(_powered); // x ^ (1 / 16) is the same as 16th root of x + + // given `change = kFactor * (score - expectedScore)` we can distribute kFactor to both terms + kExpectedScore = _kFactor / (100 + powered); // both numerator and denominator scaled up by 100 + kScore = kFactor * score; // input score is already scaled up by 100 + + // determines the sign of the ELO change + negative = kScore < kExpectedScore; + change = negative ? kExpectedScore - kScore : kScore - kExpectedScore; + } + } +} \ No newline at end of file diff --git a/contracts/libs/FixedPointMathLib.sol b/contracts/libs/FixedPointMathLib.sol new file mode 100644 index 0000000..6887722 --- /dev/null +++ b/contracts/libs/FixedPointMathLib.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Arithmetic library with operations for fixed-point numbers. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) +/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) +library FixedPointMathLib { + /*////////////////////////////////////////////////////////////// + SIMPLIFIED FIXED POINT OPERATIONS + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant MAX_UINT256 = 2**256 - 1; + + uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s. + + function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down. + } + + function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up. + } + + function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down. + } + + function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up. + } + + /*////////////////////////////////////////////////////////////// + LOW LEVEL FIXED POINT OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function mulDivDown( + uint256 x, + uint256 y, + uint256 denominator + ) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) + if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { + revert(0, 0) + } + + // Divide x * y by the denominator. + z := div(mul(x, y), denominator) + } + } + + function mulDivUp( + uint256 x, + uint256 y, + uint256 denominator + ) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) + if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { + revert(0, 0) + } + + // If x * y modulo the denominator is strictly greater than 0, + // 1 is added to round up the division of x * y by the denominator. + z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator)) + } + } + + function rpow( + uint256 x, + uint256 n, + uint256 scalar + ) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + switch x + case 0 { + switch n + case 0 { + // 0 ** 0 = 1 + z := scalar + } + default { + // 0 ** n = 0 + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + // If n is even, store scalar in z for now. + z := scalar + } + default { + // If n is odd, store x in z for now. + z := x + } + + // Shifting right by 1 is like dividing by 2. + let half := shr(1, scalar) + + for { + // Shift n right by 1 before looping to halve it. + n := shr(1, n) + } n { + // Shift n right by 1 each iteration to halve it. + n := shr(1, n) + } { + // Revert immediately if x ** 2 would overflow. + // Equivalent to iszero(eq(div(xx, x), x)) here. + if shr(128, x) { + revert(0, 0) + } + + // Store x squared. + let xx := mul(x, x) + + // Round to the nearest number. + let xxRound := add(xx, half) + + // Revert if xx + half overflowed. + if lt(xxRound, xx) { + revert(0, 0) + } + + // Set x to scaled xxRound. + x := div(xxRound, scalar) + + // If n is even: + if mod(n, 2) { + // Compute z * x. + let zx := mul(z, x) + + // If z * x overflowed: + if iszero(eq(div(zx, x), z)) { + // Revert if x is non-zero. + if iszero(iszero(x)) { + revert(0, 0) + } + } + + // Round to the nearest number. + let zxRound := add(zx, half) + + // Revert if zx + half overflowed. + if lt(zxRound, zx) { + revert(0, 0) + } + + // Return properly scaled zxRound. + z := div(zxRound, scalar) + } + } + } + } + } + + /*////////////////////////////////////////////////////////////// + GENERAL NUMBER UTILITIES + //////////////////////////////////////////////////////////////*/ + + function sqrt(uint256 x) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + let y := x // We start y at x, which will help us make our initial estimate. + + z := 181 // The "correct" value is 1, but this saves a multiplication later. + + // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad + // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically. + + // We check y >= 2^(k + 8) but shift right by k bits + // each branch to ensure that if x >= 256, then y >= 256. + if iszero(lt(y, 0x10000000000000000000000000000000000)) { + y := shr(128, y) + z := shl(64, z) + } + if iszero(lt(y, 0x1000000000000000000)) { + y := shr(64, y) + z := shl(32, z) + } + if iszero(lt(y, 0x10000000000)) { + y := shr(32, y) + z := shl(16, z) + } + if iszero(lt(y, 0x1000000)) { + y := shr(16, y) + z := shl(8, z) + } + + // Goal was to get z*z*y within a small factor of x. More iterations could + // get y in a tighter range. Currently, we will have y in [256, 256*2^16). + // We ensured y >= 256 so that the relative difference between y and y+1 is small. + // That's not possible if x < 256 but we can just verify those cases exhaustively. + + // Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256. + // Correctness can be checked exhaustively for x < 256, so we assume y >= 256. + // Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps. + + // For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range + // (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256. + + // Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate + // sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18. + + // There is no overflow risk here since y < 2^136 after the first branch above. + z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181. + + // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough. + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + + // If x+1 is a perfect square, the Babylonian method cycles between + // floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor. + // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division + // Since the ceil is rare, we save gas on the assignment and repeat division in the rare case. + // If you don't care whether the floor or ceil square root is returned, you can remove this statement. + z := sub(z, lt(div(x, z), z)) + } + } + + function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // Mod x by y. Note this will return + // 0 instead of reverting if y is zero. + z := mod(x, y) + } + } + + function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) { + /// @solidity memory-safe-assembly + assembly { + // Divide x by y. Note this will return + // 0 instead of reverting if y is zero. + r := div(x, y) + } + } + + function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // Add 1 to x * y if x % y > 0. Note this will + // return 0 instead of reverting if y is zero. + z := add(gt(mod(x, y), 0), div(x, y)) + } + } +} diff --git a/contracts/token/Minter.sol b/contracts/token/Minter.sol index d342bc9..18419bd 100644 --- a/contracts/token/Minter.sol +++ b/contracts/token/Minter.sol @@ -1,17 +1,20 @@ pragma solidity ^0.8.20; // SPDX-License-Identifier: MIT +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "./IMinter.sol"; import "../contribution/IServiceNft.sol"; import "../contribution/IContributionNft.sol"; import "../virtualPersona/IAgentNft.sol"; import "../virtualPersona/IAgentToken.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Minter is IMinter, Ownable { +contract Minter is IMinter, Initializable, OwnableUpgradeable { + using SafeERC20 for IERC20; + address public serviceNft; address public contributionNft; address public agentNft; @@ -20,6 +23,8 @@ contract Minter is IMinter, Ownable { uint256 public ipShare; // Share for IP holder uint256 public impactMultiplier; + uint256 public maxImpact; + uint256 public constant DENOM = 10000; mapping(uint256 => bool) _mintedNfts; @@ -28,6 +33,12 @@ contract Minter is IMinter, Ownable { mapping(uint256 => uint256) public ipShareOverrides; bool internal locked; + event TokenSaved( + address indexed by, + address indexed receiver, + address indexed token, + uint256 amount + ); modifier noReentrant() { require(!locked, "cannot reenter"); @@ -44,23 +55,32 @@ contract Minter is IMinter, Ownable { address agentFactory; - constructor( + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( address serviceAddress, address contributionAddress, address agentAddress, - uint256 _ipShare, - uint256 _impactMultiplier, - address _ipVault, - address _agentFactory, - address initialOwner - ) Ownable(initialOwner) { + uint256 ipShare_, + uint256 impactMultiplier_, + address ipVault_, + address agentFactory_, + address initialOwner, + uint256 maxImpact_ + ) public initializer { + __Ownable_init(initialOwner); + serviceNft = serviceAddress; contributionNft = contributionAddress; agentNft = agentAddress; - ipShare = _ipShare; - impactMultiplier = _impactMultiplier; - ipVault = _ipVault; - agentFactory = _agentFactory; + ipShare = ipShare_; + impactMultiplier = impactMultiplier_; + ipVault = ipVault_; + agentFactory = agentFactory_; + maxImpact = maxImpact_; } modifier onlyFactory() { @@ -105,11 +125,15 @@ contract Minter is IMinter, Ownable { function setImpactMulOverride( uint256 virtualId, uint256 mul - ) public onlyAgentDAO(virtualId) { + ) public onlyOwner { impactMulOverrides[virtualId] = mul; emit AgentImpactMultiplierUpdated(virtualId, mul); } + function setMaxImpact(uint256 maxImpact_) public onlyOwner { + maxImpact = maxImpact_; + } + function _getImpactMultiplier( uint256 virtualId ) internal view returns (uint256) { @@ -141,9 +165,11 @@ contract Minter is IMinter, Ownable { uint256 finalImpactMultiplier = _getImpactMultiplier(virtualId); uint256 datasetId = contribution.getDatasetId(nftId); - uint256 amount = (IServiceNft(serviceNft).getImpact(nftId) * - finalImpactMultiplier * - 10 ** 18) / DENOM; + uint256 impact = IServiceNft(serviceNft).getImpact(nftId); + if (impact > maxImpact) { + impact = maxImpact; + } + uint256 amount = (impact * finalImpactMultiplier * 10 ** 18) / DENOM; uint256 dataAmount = datasetId > 0 ? (IServiceNft(serviceNft).getImpact(datasetId) * finalImpactMultiplier * @@ -154,18 +180,27 @@ contract Minter is IMinter, Ownable { // Mint to model owner if (amount > 0) { address modelOwner = IERC721(contributionNft).ownerOf(nftId); - IAgentToken(tokenAddress).mint(modelOwner, amount); + IAgentToken(tokenAddress).transfer(modelOwner, amount); } // Mint to Dataset owner if (datasetId != 0) { address datasetOwner = IERC721(contributionNft).ownerOf(datasetId); - IAgentToken(tokenAddress).mint(datasetOwner, dataAmount); + IAgentToken(tokenAddress).transfer(datasetOwner, dataAmount); } // To IP vault if (ipAmount > 0) { - IAgentToken(tokenAddress).mint(ipVault, ipAmount); + IAgentToken(tokenAddress).transfer(ipVault, ipAmount); } } + + function saveToken( + address _token, + address _receiver, + uint256 _amount + ) external onlyOwner { + IERC20(_token).safeTransfer(_receiver, _amount); + emit TokenSaved(_msgSender(), _receiver, _token, _amount); + } } diff --git a/contracts/virtualPersona/AgentDAO.sol b/contracts/virtualPersona/AgentDAO.sol index 1b86ee5..cfa2192 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -10,6 +10,9 @@ import "./GovernorCountingSimpleUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "../contribution/IContributionNft.sol"; +import "./IEloCalculator.sol"; +import "../contribution/IServiceNft.sol"; +import "./IAgentNft.sol"; contract AgentDAO is IAgentDAO, @@ -30,6 +33,8 @@ contract AgentDAO is uint256 private _totalScore; + address private _agentNft; + constructor() { _disableInitializers(); } @@ -37,7 +42,7 @@ contract AgentDAO is function initialize( string memory name, IVotes token, - address contributionNft, + address agentNft, uint256 threshold, uint32 votingPeriod_ ) external initializer { @@ -49,7 +54,7 @@ contract AgentDAO is __GovernorCountingSimple_init(); __GovernorStorage_init(); - _contributionNft = contributionNft; + _agentNft = agentNft; } // The following functions are overrides required by Solidity. @@ -95,9 +100,10 @@ contract AgentDAO is uint256 proposerVotes = getVotes(proposer, clock() - 1); uint256 votesThreshold = proposalThreshold(); + address contributionNft = IAgentNft(_agentNft).getContributionNft(); if ( proposerVotes < votesThreshold && - proposer != IContributionNft(_contributionNft).getAdmin() + proposer != IContributionNft(contributionNft).getAdmin() ) { revert GovernorInsufficientProposerVotes( proposer, @@ -200,25 +206,50 @@ contract AgentDAO is bytes memory params ) internal { // Check is this a contribution proposal - address owner = IERC721(_contributionNft).ownerOf(proposalId); + address contributionNft = IAgentNft(_agentNft).getContributionNft(); + address owner = IERC721(contributionNft).ownerOf(proposalId); if (owner == address(0)) { return; } - bool isModel = IContributionNft(_contributionNft).isModel(proposalId); + bool isModel = IContributionNft(contributionNft).isModel(proposalId); if (!isModel) { return; } - (uint256 maturity, uint8[] memory votes) = abi.decode( - params, - (uint256, uint8[]) - ); + uint8[] memory votes = abi.decode(params, (uint8[])); + uint256 maturity = _calcMaturity(proposalId, votes); + _proposalMaturities[proposalId] += (maturity * weight); emit ValidatorEloRating(proposalId, account, maturity, votes); } + function _calcMaturity( + uint256 proposalId, + uint8[] memory votes + ) internal view returns (uint256) { + address contributionNft = IAgentNft(_agentNft).getContributionNft(); + address serviceNft = IAgentNft(_agentNft).getServiceNft(); + uint256 virtualId = IContributionNft(contributionNft).tokenVirtualId( + proposalId + ); + uint8 core = IContributionNft(contributionNft).getCore(proposalId); + uint256 coreService = IServiceNft(serviceNft).getCoreService( + virtualId, + core + ); + // All services start with 100 maturity + uint256 maturity = 100; + if (coreService > 0) { + maturity = IServiceNft(serviceNft).getMaturity(coreService); + maturity = IEloCalculator(IAgentNft(_agentNft).getEloCalculator()) + .battleElo(maturity, votes); + } + + return maturity; + } + function getMaturity(uint256 proposalId) public view returns (uint256) { (, uint256 forVotes, ) = proposalVotes(proposalId); return Math.min(10000, _proposalMaturities[proposalId] / forVotes); diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index cb91e34..0dcbd9e 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -6,8 +6,8 @@ import "@openzeppelin/contracts/governance/IGovernor.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "./IAgentFactory.sol"; import "./IAgentToken.sol"; @@ -15,9 +15,13 @@ import "./IAgentVeToken.sol"; import "./IAgentDAO.sol"; import "./IAgentNft.sol"; import "../libs/IERC6551Registry.sol"; -import "../token/IMinter.sol"; -contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { +contract AgentFactoryV2 is + IAgentFactory, + Initializable, + AccessControl, + PausableUpgradeable +{ using SafeERC20 for IERC20; uint256 private _nextId; @@ -94,18 +98,17 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { /////////////////////////////////////////////////////////////// // V2 Storage /////////////////////////////////////////////////////////////// - address private _uniswapRouter; - address private _minter; - address private _tokenAdmin; - address[] public allTradingTokens; + address private _uniswapRouter; address public veTokenImplementation; - address public defaultDelegatee; + address private _minter; // Unused + address private _tokenAdmin; + address public defaultDelegatee; // Unused // Default agent token params bytes private _tokenSupplyParams; bytes private _tokenTaxParams; - uint16 private _tokenMultiplier; + uint16 private _tokenMultiplier; // Unused /////////////////////////////////////////////////////////////// @@ -124,6 +127,8 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { uint256 applicationThreshold_, address vault_ ) public initializer { + __Pausable_init(); + tokenImplementation = tokenImplementation_; veTokenImplementation = veTokenImplementation_; daoImplementation = daoImplementation_; @@ -134,7 +139,6 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { _nextId = 1; _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _vault = vault_; - _tokenMultiplier = 10000; } function getApplication( @@ -152,7 +156,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { address tbaImplementation, uint32 daoVotingPeriod, uint256 daoThreshold - ) public returns (uint256) { + ) public whenNotPaused returns (uint256) { address sender = _msgSender(); require( IERC20(assetToken).balanceOf(sender) >= applicationThreshold, @@ -249,16 +253,13 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); uint256 initialAmount = application.withdrawableAmount; - uint256 initialSupply = (initialAmount * _tokenMultiplier) / 10000; // Initial LP supply - application.withdrawableAmount = 0; application.status = ApplicationStatus.Executed; // C1 address token = _createNewAgentToken( application.name, - application.symbol, - initialSupply + application.symbol ); // C2 @@ -319,7 +320,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { IAgentVeToken(veToken).stake( IERC20(lp).balanceOf(address(this)), application.proposer, - defaultDelegatee + application.proposer ); emit NewPersona(virtualId, token, dao, tbaAddress, veToken, lp); @@ -335,7 +336,7 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { IAgentDAO(instance).initialize( name, token, - IAgentNft(nft).getContributionNft(), + nft, daoThreshold, daoVotingPeriod ); @@ -346,17 +347,14 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { function _createNewAgentToken( string memory name, - string memory symbol, - uint256 initialSupply + string memory symbol ) internal returns (address instance) { - instance = Clones.clone(tokenImplementation); IAgentToken(instance).initialize( [_tokenAdmin, _uniswapRouter, assetToken], abi.encode(name, symbol), _tokenSupplyParams, - _tokenTaxParams, - initialSupply + _tokenTaxParams ); allTradingTokens.push(instance); @@ -410,14 +408,6 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { veTokenImplementation = veToken; } - function setMinter(address newMinter) public onlyRole(DEFAULT_ADMIN_ROLE) { - _minter = newMinter; - } - - function minter() public view override returns (address) { - return _minter; - } - function setMaturityDuration( uint256 newDuration ) public onlyRole(DEFAULT_ADMIN_ROLE) { @@ -437,14 +427,22 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { } function setTokenSupplyParams( + uint256 maxSupply, + uint256 lpSupply, + uint256 vaultSupply, uint256 maxTokensPerWallet, uint256 maxTokensPerTxn, - uint256 botProtectionDurationInSeconds + uint256 botProtectionDurationInSeconds, + address vault ) public onlyRole(DEFAULT_ADMIN_ROLE) { _tokenSupplyParams = abi.encode( + maxSupply, + lpSupply, + vaultSupply, maxTokensPerWallet, maxTokensPerTxn, - botProtectionDurationInSeconds + botProtectionDurationInSeconds, + vault ); } @@ -462,21 +460,35 @@ contract AgentFactoryV2 is IAgentFactory, Initializable, AccessControl { ); } - function setTokenMultiplier( - uint16 newMultiplier + function setAssetToken( + address newToken ) public onlyRole(DEFAULT_ADMIN_ROLE) { - _tokenMultiplier = newMultiplier; + assetToken = newToken; } - function setDefaultDelegatee( - address newDelegatee - ) public onlyRole(DEFAULT_ADMIN_ROLE) { - defaultDelegatee = newDelegatee; + function pause() public onlyRole(DEFAULT_ADMIN_ROLE) { + _pause(); } - function setAssetToken( - address newToken - ) public onlyRole(DEFAULT_ADMIN_ROLE) { - assetToken = newToken; + function unpause() public onlyRole(DEFAULT_ADMIN_ROLE) { + _unpause(); + } + + function _msgSender() + internal + view + override(Context, ContextUpgradeable) + returns (address sender) + { + sender = ContextUpgradeable._msgSender(); + } + + function _msgData() + internal + view + override(Context, ContextUpgradeable) + returns (bytes calldata) + { + return ContextUpgradeable._msgData(); } } diff --git a/contracts/virtualPersona/AgentNftV2.sol b/contracts/virtualPersona/AgentNftV2.sol index 2c9ed19..aeed359 100644 --- a/contracts/virtualPersona/AgentNftV2.sol +++ b/contracts/virtualPersona/AgentNftV2.sol @@ -53,6 +53,7 @@ contract AgentNftV2 is bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); mapping(uint256 => bool) private _blacklists; mapping(uint256 => VirtualLP) public virtualLPs; + address private _eloCalculator; event AgentBlacklisted(uint256 indexed virtualId, bool value); @@ -61,10 +62,6 @@ contract AgentNftV2 is _disableInitializers(); } - function kk() public pure returns (string memory) { - return "kk"; - } - function initialize(address defaultAdmin) public initializer { __ERC721_init("Agent", "AGENT"); __ERC721URIStorage_init(); @@ -77,6 +74,7 @@ contract AgentNftV2 is __AccessControl_init(); _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); _grantRole(VALIDATOR_ADMIN_ROLE, defaultAdmin); + _grantRole(ADMIN_ROLE, defaultAdmin); _nextVirtualId = 1; } @@ -300,4 +298,14 @@ contract AgentNftV2 is _getPastValidatorScore ); } + + function setEloCalculator( + address eloCalculator + ) public onlyRole(ADMIN_ROLE) { + _eloCalculator = eloCalculator; + } + + function getEloCalculator() public view returns (address) { + return _eloCalculator; + } } diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol index e0cb02c..2185142 100644 --- a/contracts/virtualPersona/AgentToken.sol +++ b/contracts/virtualPersona/AgentToken.sol @@ -46,6 +46,7 @@ contract AgentToken is uint128 public maxTokensPerWallet; address public projectTaxRecipient; uint128 public projectTaxPendingSwap; + address public vault; // Project supply vault string private _name; string private _symbol; @@ -68,11 +69,6 @@ contract AgentToken is IAgentFactory private _factory; // Single source of truth - modifier onlyMinter() { - require(_msgSender() == _factory.minter(), "Caller is not the minter"); - _; - } - /** * @dev {onlyOwnerOrFactory} * @@ -93,8 +89,7 @@ contract AgentToken is address[3] memory integrationAddresses_, bytes memory baseParams_, bytes memory supplyParams_, - bytes memory taxParams_, - uint256 lpSupply_ + bytes memory taxParams_ ) external initializer { _decodeBaseParams(integrationAddresses_[0], baseParams_); _uniswapRouter = IUniswapV2Router02(integrationAddresses_[1]); @@ -110,6 +105,10 @@ contract AgentToken is (ERC20TaxParameters) ); + _processSupplyParams(supplyParams); + + uint256 lpSupply = supplyParams.lpSupply * (10 ** decimals()); + uint256 vaultSupply = supplyParams.vaultSupply * (10 ** decimals()); maxTokensPerWallet = uint128( supplyParams.maxTokensPerWallet * (10 ** decimals()) ); @@ -126,7 +125,7 @@ contract AgentToken is ); projectTaxRecipient = taxParams.projectTaxRecipient; - _mintBalances(lpSupply_); + _mintBalances(lpSupply, vaultSupply); uniswapV2Pair = _createPair(); @@ -161,9 +160,24 @@ contract AgentToken is function _processSupplyParams( ERC20SupplyParameters memory erc20SupplyParameters_ ) internal { + if ( + erc20SupplyParameters_.maxSupply != + (erc20SupplyParameters_.vaultSupply + + erc20SupplyParameters_.lpSupply) + ) { + revert SupplyTotalMismatch(); + } + + if (erc20SupplyParameters_.maxSupply > type(uint128).max) { + revert MaxSupplyTooHigh(); + } + _unlimited.add(owner()); _unlimited.add(address(this)); _unlimited.add(address(0)); + + vault = erc20SupplyParameters_.vault; + _unlimited.add(vault); } /** @@ -204,10 +218,14 @@ contract AgentToken is * * @param lpMint_ The number of tokens for liquidity */ - function _mintBalances(uint256 lpMint_) internal { + function _mintBalances(uint256 lpMint_, uint256 vaultMint_) internal { if (lpMint_ > 0) { _mint(address(this), lpMint_); } + + if (vaultMint_ > 0) { + _mint(vault, vaultMint_); + } } /** @@ -1003,7 +1021,7 @@ contract AgentToken is } return (amountLessTax_); } - + /** * @dev function {_autoSwap} * @@ -1380,8 +1398,4 @@ contract AgentToken is ) internal virtual {} receive() external payable {} - - function mint(address account, uint256 value) public onlyMinter { - _mint(account, value); - } } diff --git a/contracts/virtualPersona/EloCalculator.sol b/contracts/virtualPersona/EloCalculator.sol new file mode 100644 index 0000000..dcc61d0 --- /dev/null +++ b/contracts/virtualPersona/EloCalculator.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Elo} from "../libs/Elo.sol"; +import "./IEloCalculator.sol"; + +contract EloCalculator is IEloCalculator, Initializable, OwnableUpgradeable { + uint256 public k; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address initialOwner) public initializer { + __Ownable_init(initialOwner); + k = 30; + } + + function mapBattleResultToGameResult( + uint8 result + ) internal pure returns (uint256) { + if (result == 1) { + return 100; + } else if (result == 2) { + return 50; + } else if (result == 3) { + return 50; + } + return 0; + } + + function _roundUp( + uint256 numerator, + uint256 denominator + ) internal pure returns (uint256) { + return (numerator + denominator - 1) / denominator; + } + + // Get winner elo rating + function battleElo( + uint256 currentRating, + uint8[] memory battles + ) public view returns (uint256) { + uint256 eloA = 1000; + uint256 eloB = 1000; + for (uint256 i = 0; i < battles.length; i++) { + uint256 result = mapBattleResultToGameResult(battles[i]); + (uint256 change, bool negative) = Elo.ratingChange( + eloB, + eloA, + result, + k + ); + change = _roundUp(change, 100); + if (negative) { + eloA -= change; + eloB += change; + } else { + eloA += change; + eloB -= change; + } + } + return currentRating + eloA - 1000; + } + + function setK(uint256 k_) public onlyOwner { + k = k_; + } +} diff --git a/contracts/virtualPersona/IAgentDAO.sol b/contracts/virtualPersona/IAgentDAO.sol index 5c3f098..0e0bc1f 100644 --- a/contracts/virtualPersona/IAgentDAO.sol +++ b/contracts/virtualPersona/IAgentDAO.sol @@ -8,7 +8,7 @@ interface IAgentDAO { function initialize( string memory name, IVotes token, - address contributionNft, + address agentNft, uint256 threshold, uint32 votingPeriod_ ) external; diff --git a/contracts/virtualPersona/IAgentFactory.sol b/contracts/virtualPersona/IAgentFactory.sol index 37452f3..0578a79 100644 --- a/contracts/virtualPersona/IAgentFactory.sol +++ b/contracts/virtualPersona/IAgentFactory.sol @@ -18,6 +18,4 @@ interface IAgentFactory { function withdraw(uint256 id) external; function totalAgents() external view returns (uint256); - - function minter() external view returns (address); } diff --git a/contracts/virtualPersona/IAgentNft.sol b/contracts/virtualPersona/IAgentNft.sol index a328465..c88a9a8 100644 --- a/contracts/virtualPersona/IAgentNft.sol +++ b/contracts/virtualPersona/IAgentNft.sol @@ -66,4 +66,6 @@ interface IAgentNft is IValidatorRegistry { function nextVirtualId() external view returns (uint256); function isBlacklisted(uint256 virtualId) external view returns (bool); + + function getEloCalculator() external view returns (address); } diff --git a/contracts/virtualPersona/IAgentToken.sol b/contracts/virtualPersona/IAgentToken.sol index 2d81b5f..5f58a5c 100644 --- a/contracts/virtualPersona/IAgentToken.sol +++ b/contracts/virtualPersona/IAgentToken.sol @@ -346,17 +346,6 @@ interface IAgentToken is address[3] memory integrationAddresses_, bytes memory baseParams_, bytes memory supplyParams_, - bytes memory taxParams_, - uint256 lpSupply_ + bytes memory taxParams_ ) external; - - /** - * @dev {mint} - * - * Allow minter to mint tokens on contribution - * - * @param account The recipient - * @param value mint amount - */ - function mint(address account, uint256 value) external; } diff --git a/contracts/virtualPersona/IERC20Config.sol b/contracts/virtualPersona/IERC20Config.sol index 64fa0c8..6d88a12 100644 --- a/contracts/virtualPersona/IERC20Config.sol +++ b/contracts/virtualPersona/IERC20Config.sol @@ -15,9 +15,13 @@ interface IERC20Config { } struct ERC20SupplyParameters { + uint256 maxSupply; + uint256 lpSupply; + uint256 vaultSupply; uint256 maxTokensPerWallet; uint256 maxTokensPerTxn; uint256 botProtectionDurationInSeconds; + address vault; } struct ERC20TaxParameters { diff --git a/contracts/virtualPersona/IEloCalculator.sol b/contracts/virtualPersona/IEloCalculator.sol new file mode 100644 index 0000000..474883d --- /dev/null +++ b/contracts/virtualPersona/IEloCalculator.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IEloCalculator { + function battleElo( + uint256 currentRating, + uint8[] memory battles + ) external view returns (uint256); +} diff --git a/scripts/arguments/elo.js b/scripts/arguments/elo.js new file mode 100644 index 0000000..121084d --- /dev/null +++ b/scripts/arguments/elo.js @@ -0,0 +1 @@ +module.exports = [process.env.ADMIN]; diff --git a/scripts/arguments/minter.js b/scripts/arguments/minter.js index eb66b4a..5bea7ee 100644 --- a/scripts/arguments/minter.js +++ b/scripts/arguments/minter.js @@ -7,4 +7,5 @@ module.exports = [ process.env.IP_VAULT, process.env.VIRTUAL_FACTORY, process.env.ADMIN, -]; + process.env.MAX_IMPACT, +]; \ No newline at end of file diff --git a/scripts/v2/deployElo.ts b/scripts/v2/deployElo.ts new file mode 100644 index 0000000..c59f0aa --- /dev/null +++ b/scripts/v2/deployElo.ts @@ -0,0 +1,20 @@ +import { ethers, upgrades } from "hardhat"; + +const adminSigner = new ethers.Wallet( + process.env.ADMIN_PRIVATE_KEY, + ethers.provider +); + +(async () => { + try { + const args = require("../arguments/elo"); + const Contract = await ethers.getContractFactory("EloCalculator"); + const contract = await upgrades.deployProxy(Contract, args, { + initialOwner: process.env.CONTRACT_CONTROLLER, + }); + await contract.waitForDeployment(); + console.log("EloCalculator deployed to:", contract.target); + } catch (e) { + console.log(e); + } +})(); diff --git a/scripts/v2/deployMinter.ts b/scripts/v2/deployMinter.ts new file mode 100644 index 0000000..c148826 --- /dev/null +++ b/scripts/v2/deployMinter.ts @@ -0,0 +1,20 @@ +import { ethers, upgrades } from "hardhat"; + +const adminSigner = new ethers.Wallet( + process.env.ADMIN_PRIVATE_KEY, + ethers.provider +); + +(async () => { + try { + const args = require("../arguments/minter"); + const Contract = await ethers.getContractFactory("Minter"); + const contract = await upgrades.deployProxy(Contract, args, { + initialOwner: process.env.CONTRACT_CONTROLLER, + }); + await contract.waitForDeployment(); + console.log("Minter deployed to:", contract.target); + } catch (e) { + console.log(e); + } +})(); diff --git a/scripts/v2/migrateV2.ts b/scripts/v2/migrateV2.ts index 5990775..0980594 100644 --- a/scripts/v2/migrateV2.ts +++ b/scripts/v2/migrateV2.ts @@ -23,14 +23,19 @@ async function upgradeFactory(implementations, assetToken) { implementations.daoImpl.target ); } + if (assetToken) { await factory.setAssetToken(assetToken); } await factory.setTokenAdmin(process.env.ADMIN); await factory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LP_SUPPLY, + process.env.AGENT_TOKEN_VAULT_SUPPLY, process.env.AGENT_TOKEN_LIMIT, process.env.AGENT_TOKEN_LIMIT, - process.env.BOT_PROTECTION + process.env.BOT_PROTECTION, + process.env.MINTER ); await factory.setTokenTaxParams( process.env.TAX, @@ -38,8 +43,7 @@ async function upgradeFactory(implementations, assetToken) { process.env.SWAP_THRESHOLD, process.env.TAX_VAULT ); - await factory.setTokenMultiplier(10000); - await factory.setDefaultDelegatee(process.env.ADMIN); + console.log("Upgraded FactoryV2", factory.target); } @@ -80,6 +84,12 @@ async function upgradeAgentNft() { ); await proxyAdmin.upgradeAndCall(process.env.VIRTUAL_NFT, nft.target, "0x"); console.log("Upgraded AgentNft"); + + const contract = await ethers.getContractAt( + "AgentNftV2", + process.env.VIRTUAL_NFT + ); + await contract.setEloCalculator(process.env.ELO); } async function deployImplementations() { @@ -87,23 +97,23 @@ async function deployImplementations() { await daoImpl.waitForDeployment(); console.log("AgentDAO deployed to:", daoImpl.target); - const tokenImpl = await ethers.deployContract("AgentToken"); - await tokenImpl.waitForDeployment(); - console.log("AgentToken deployed to:", tokenImpl.target); + // const tokenImpl = await ethers.deployContract("AgentToken"); + // await tokenImpl.waitForDeployment(); + // console.log("AgentToken deployed to:", tokenImpl.target); - const veTokenImpl = await ethers.deployContract("AgentVeToken"); - await veTokenImpl.waitForDeployment(); - console.log("AgentVeToken deployed to:", veTokenImpl.target); + // const veTokenImpl = await ethers.deployContract("AgentVeToken"); + // await veTokenImpl.waitForDeployment(); + // console.log("AgentVeToken deployed to:", veTokenImpl.target); - return { daoImpl, tokenImpl, veTokenImpl }; + return { daoImpl, tokenImpl: null, veTokenImpl: null }; } (async () => { try { - // const implementations = await deployImplementations(); - // await upgradeFactory(implementations); + //const implementations = await deployImplementations(); + await upgradeFactory(); //await deployAgentNft(); - await upgradeAgentNft(); + //await upgradeAgentNft(); } catch (e) { console.log(e); } diff --git a/test/ProtocolDAO.js b/test/ProtocolDAO.js index ac97e5b..b4e3e62 100644 --- a/test/ProtocolDAO.js +++ b/test/ProtocolDAO.js @@ -198,7 +198,7 @@ describe("ProtocolDAO", function () { const voterEtherBalance = await ethers.provider.getBalance(voter); expect(voterEtherBalance).to.be.lt(10000000000000000000000n); const relayerEtherBalance = await ethers.provider.getBalance(relayer); - expect(relayerEtherBalance).to.be.equal(10000000000000000000000n); + expect(relayerEtherBalance).to.be.lessThan(10000000000000000000000n); // Voter sign vote diff --git a/test/agentDAO.js b/test/agentDAO.js index 72cbf89..863cb41 100644 --- a/test/agentDAO.js +++ b/test/agentDAO.js @@ -1,25 +1,10 @@ -/* -Test scenario: -1. Accounts: [validator1, staker1, validator2, staker2] -2. Stakes: [100000, 2000, 5000, 20000] -3. Uptime: [3,1] -4. All contribution NFTs are owned by account #10 -*/ const { expect } = require("chai"); const { toBeHex } = require("ethers/utils"); -const abi = ethers.AbiCoder.defaultAbiCoder(); const { loadFixture, mine, } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); -const { parseEther, formatEther } = require("ethers"); - -const getExecuteCallData = (factory, proposalId) => { - return factory.interface.encodeFunctionData("executeApplication", [ - proposalId, - false, - ]); -}; +const { parseEther } = require("ethers"); const getMintServiceCalldata = async (serviceNft, virtualId, hash) => { return serviceNft.interface.encodeFunctionData("mint", [virtualId, hash]); @@ -31,9 +16,6 @@ function getDescHash(str) { describe("AgentDAO", function () { const PROPOSAL_THRESHOLD = parseEther("100000"); //100k - const MATURITY_SCORE = toBeHex(2000, 32); // 20% - - const TOKEN_URI = "http://jessica"; const genesisInput = { name: "Jessica", @@ -107,7 +89,6 @@ describe("AgentDAO", function () { await agentDAO.waitForDeployment(); const agentVeToken = await ethers.deployContract("AgentVeToken"); await agentVeToken.waitForDeployment(); - const agentFactory = await upgrades.deployProxy( await ethers.getContractFactory("AgentFactoryV2"), [ @@ -123,25 +104,32 @@ describe("AgentDAO", function () { ); await agentFactory.waitForDeployment(); await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); - const minter = await ethers.deployContract("Minter", [ - service.target, - contribution.target, - agentNft.target, - process.env.IP_SHARES, - process.env.IMPACT_MULTIPLIER, - ipVault.address, - agentFactory.target, - deployer.address, - ]); + const minter = await upgrades.deployProxy( + await ethers.getContractFactory("Minter"), + [ + service.target, + contribution.target, + agentNft.target, + process.env.IP_SHARES, + process.env.IMPACT_MULTIPLIER, + ipVault.address, + agentFactory.target, + deployer.address, + process.env.MAX_IMPACT, + ] + ); await minter.waitForDeployment(); - await agentFactory.setMinter(minter.target); await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); await agentFactory.setTokenAdmin(deployer.address); await agentFactory.setTokenSupplyParams( process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LP_SUPPLY, + process.env.AGENT_TOKEN_VAULT_SUPPLY, process.env.AGENT_TOKEN_LIMIT, - process.env.BOT_PROTECTION + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION, + minter.target ); await agentFactory.setTokenTaxParams( process.env.TAX, @@ -149,8 +137,6 @@ describe("AgentDAO", function () { process.env.SWAP_THRESHOLD, treasury.address ); - await agentFactory.setDefaultDelegatee(deployer.address); - return { virtualToken, agentFactory, @@ -224,15 +210,8 @@ describe("AgentDAO", function () { it("should auto early execution when forVotes == totalSupply", async function () { const base = await loadFixture(deployWithAgent); - const { founder, deployer } = await getAccounts(); - const { - agent, - serviceNft, - contributionNft, - minter, - virtualToken, - agentFactory, - } = base; + const { founder } = await getAccounts(); + const { agent, serviceNft } = base; const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); const desc = "test"; const descHash = getDescHash(desc); @@ -251,9 +230,10 @@ describe("AgentDAO", function () { // Proposal not executed yet await expect(serviceNft.ownerOf(proposalId)).to.be.reverted; - await mine(10); - await agentDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", "0x"); + await agentDAO + .connect(founder) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", "0x"); // Proposal should be auto executed expect(await serviceNft.ownerOf(proposalId)).to.equal(agent.tba); diff --git a/test/contribution.js b/test/contribution.js index be55a00..18a4120 100644 --- a/test/contribution.js +++ b/test/contribution.js @@ -14,7 +14,6 @@ const getSetImpactMulCalldata = async (minter, virtualId, multiplier) => { ]); }; - const getSetIPShareCalldata = async (minter, virtualId, share) => { return minter.interface.encodeFunctionData("setIPShareOverride", [ virtualId, @@ -82,8 +81,14 @@ describe("Contribution", function () { ); await virtualToken.waitForDeployment(); + const EloCalculator = await ethers.getContractFactory("EloCalculator"); + const eloCalculator = await upgrades.deployProxy(EloCalculator, [ + deployer.address, + ]); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); + await agentNft.connect(deployer).setEloCalculator(eloCalculator.target); const contribution = await upgrades.deployProxy( await ethers.getContractFactory("ContributionNft"), @@ -122,25 +127,32 @@ describe("Contribution", function () { ); await agentFactory.waitForDeployment(); await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); - const minter = await ethers.deployContract("Minter", [ - service.target, - contribution.target, - agentNft.target, - process.env.IP_SHARES, - process.env.IMPACT_MULTIPLIER, - ipVault.address, - agentFactory.target, - deployer.address - ]); + const minter = await upgrades.deployProxy( + await ethers.getContractFactory("Minter"), + [ + service.target, + contribution.target, + agentNft.target, + process.env.IP_SHARES, + process.env.IMPACT_MULTIPLIER, + ipVault.address, + agentFactory.target, + deployer.address, + process.env.MAX_IMPACT, + ] + ); await minter.waitForDeployment(); - await agentFactory.setMinter(minter.target); await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); await agentFactory.setTokenAdmin(deployer.address); await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LP_SUPPLY, + process.env.AGENT_TOKEN_VAULT_SUPPLY, process.env.AGENT_TOKEN_LIMIT, process.env.AGENT_TOKEN_LIMIT, - process.env.BOT_PROTECTION + process.env.BOT_PROTECTION, + minter.target ); await agentFactory.setTokenTaxParams( process.env.TAX, @@ -255,20 +267,19 @@ describe("Contribution", function () { async function createContribution( virtualId, coreId, - maturity, parentId, isModel, datasetId, desc, base, account, - voters + voters, + votes ) { const { serviceNft, contributionNft, minter, agentNft } = base; const daoAddr = (await agentNft.virtualInfo(virtualId)).dao; const veAddr = (await agentNft.virtualLP(virtualId)).veToken; const agentDAO = await ethers.getContractAt("AgentDAO", daoAddr); - const veToken = await ethers.getContractAt("AgentVeToken", veAddr); const descHash = getDescHash(desc); @@ -294,9 +305,7 @@ describe("Contribution", function () { isModel, datasetId ); - const voteParams = isModel - ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) - : "0x"; + const voteParams = isModel ? abi.encode(["uint8[] memory"], [votes]) : "0x"; for (const voter of voters) { await agentDAO @@ -323,14 +332,9 @@ describe("Contribution", function () { this.signers = signers; }); - it("should be able to mint a new contribution", async function () { + it("should be able to mint a new contribution NFT", async function () { const base = await loadFixture(deployWithAgent); - const { contributor1, founder, contributior1 } = await getAccounts(); - const maturity = 55; - const agentToken = await ethers.getContractAt( - "AgentToken", - base.agent.token - ); + const { contributor1, founder } = await getAccounts(); const { contributionNft, agent } = base; const veAddr = agent.veToken; const veToken = await ethers.getContractAt("AgentVeToken", veAddr); @@ -341,14 +345,14 @@ describe("Contribution", function () { const contributionId = await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance2 = await contributionNft.balanceOf(contributor1.address); @@ -361,7 +365,6 @@ describe("Contribution", function () { it("should mint agent token for successful model contribution", async function () { const base = await loadFixture(deployWithAgent); const { contributor1, founder } = await getAccounts(); - const maturity = 55; const agentToken = await ethers.getContractAt( "AgentToken", base.agent.token @@ -371,273 +374,227 @@ describe("Contribution", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance2 = await agentToken.balanceOf(contributor1.address); - expect(balance2).to.equal(parseEther(maturity.toString())); + expect(balance2).to.equal(parseEther("100000")); }); it("should mint agent token for IP owner on successful contribution", async function () { const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1, founder, agentVeToken } = - await getAccounts(); - const maturity = 55; + const { ipVault, contributor1, founder } = await getAccounts(); const agentToken = await ethers.getContractAt( "AgentToken", base.agent.token ); const balance1 = await agentToken.balanceOf(ipVault.address); expect(balance1).to.equal(0n); - await await createContribution( + await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance2 = await agentToken.balanceOf(ipVault.address); - expect(balance2).to.equal(parseEther((maturity * 0.1).toString())); + expect(balance2).to.equal(parseEther("10000")); }); it("should mint agent token for model & dataset contribution", async function () { const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1, contributor2, founder, agentVeToken } = - await getAccounts(); - const { contributionNft, serviceNft } = base; - const maturity = 100; + const { contributor1, contributor2, founder } = await getAccounts(); const agentToken = await ethers.getContractAt( "AgentToken", base.agent.token ); - const veToken = await ethers.getContractAt( - "AgentVeToken", - base.agent.veToken - ); - const agentDAO = await ethers.getContractAt("AgentDAO", base.agent.dao); // No agent token minted for dataset contribution const c1 = await createContribution( 1, 0, - maturity, 0, false, 0, "Dataset", base, contributor1.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance1 = await agentToken.balanceOf(contributor1.address); expect(balance1).to.equal(0n); - const c2 = await createContribution( + await createContribution( 1, 0, - maturity, 0, true, c1, "Test model", base, contributor2.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance2 = await agentToken.balanceOf(contributor2.address); - expect(balance2).to.equal(parseEther("30")); + expect(balance2).to.equal(parseEther("30000")); const balance12 = await agentToken.balanceOf(contributor1.address); - expect(balance12).to.equal(parseEther("70")); + expect(balance12).to.equal(parseEther("70000")); }); it("should allow adjusting global agent token multiplier", async function () { const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1, contributor2, founder, agentVeToken } = - await getAccounts(); - const { contributionNft, serviceNft, minter } = base; - const maturity = 100; + const { contributor1, contributor2, founder } = await getAccounts(); + const { minter } = base; const agentToken = await ethers.getContractAt( "AgentToken", base.agent.token ); - await minter.setImpactMultiplier(20000); //2x + await minter.setImpactMultiplier(20000000); //2x // No agent token minted for dataset contribution const c1 = await createContribution( 1, 0, - maturity, 0, false, 0, "Dataset", base, contributor1.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance1 = await agentToken.balanceOf(contributor1.address); expect(balance1).to.equal(0n); - const c2 = await createContribution( + await createContribution( 1, 0, - maturity, 0, true, c1, "Test model", base, contributor2.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance2 = await agentToken.balanceOf(contributor2.address); - expect(balance2).to.equal(parseEther("60")); + expect(balance2).to.equal(parseEther("60000")); const balance12 = await agentToken.balanceOf(contributor1.address); - expect(balance12).to.equal(parseEther("140")); + expect(balance12).to.equal(parseEther("140000")); }); it("should allow adjusting agent token multiplier", async function () { const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1, contributor2, founder, agentVeToken } = - await getAccounts(); - const { contributionNft, serviceNft, minter, agent } = base; - const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); - const maturity = 100; + const { contributor1, contributor2, founder } = await getAccounts(); + const { minter, agent } = base; const agentToken = await ethers.getContractAt("AgentToken", agent.token); - - await veToken.connect(founder).delegate(founder.address); - - const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); - - const calldata = await getSetImpactMulCalldata( - minter, - agent.virtualId, - 20000n - ); - - await agentDAO.propose( - [minter.target], - [0], - [calldata], - "Set agent token multiplier" - ); - const filter = agentDAO.filters.ProposalCreated; - const events = await agentDAO.queryFilter(filter, -1); - const event = events[0]; - const proposalId = event.args[0]; - - await mine(1); - await agentDAO.connect(founder).castVote(proposalId, 1); - await mine(1); - + await minter.setImpactMulOverride(agent.virtualId, 20000000); //2x // No agent token minted for dataset contribution const c1 = await createContribution( 1, 0, - maturity, 0, false, 0, "Dataset", base, contributor1.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance1 = await agentToken.balanceOf(contributor1.address); expect(balance1).to.equal(0n); - const c2 = await createContribution( + await createContribution( 1, 0, - maturity, 0, true, c1, "Test model", base, contributor2.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance2 = await agentToken.balanceOf(contributor2.address); - expect(balance2).to.equal(parseEther("60")); + expect(balance2).to.equal(parseEther("60000")); const balance12 = await agentToken.balanceOf(contributor1.address); - expect(balance12).to.equal(parseEther("140")); + expect(balance12).to.equal(parseEther("140000")); }); it("should not allow adjusting agent token multiplier by public", async function () { const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1, contributor2, founder, agentVeToken } = - await getAccounts(); - const { deployer, minter, agent } = base; - const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); - const maturity = 100; + const { contributor1, contributor2, founder } = await getAccounts(); + const { minter, agent } = base; const agentToken = await ethers.getContractAt("AgentToken", agent.token); await expect( - minter.setImpactMulOverride(agent.virtualId, 20000) - ).to.be.revertedWith("Only Agent DAO can operate"); + minter.connect(founder).setImpactMulOverride(agent.virtualId, 20000) + ).to.be.reverted; // No agent token minted for dataset contribution const c1 = await createContribution( 1, 0, - maturity, 0, false, 0, "Dataset", base, contributor1.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance1 = await agentToken.balanceOf(contributor1.address); expect(balance1).to.equal(0n); - const c2 = await createContribution( + await createContribution( 1, 0, - maturity, 0, true, c1, "Test model", base, contributor2.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance2 = await agentToken.balanceOf(contributor2.address); - expect(balance2).to.equal(parseEther("30")); + expect(balance2).to.equal(parseEther("30000")); const balance12 = await agentToken.balanceOf(contributor1.address); - expect(balance12).to.equal(parseEther("70")); + expect(balance12).to.equal(parseEther("70000")); }); it("should be able to adjust the global IP share", async function () { const base = await loadFixture(deployWithAgent); - const { deployer, contributor1, ipVault, founder, agentVeToken } = - await getAccounts(); - const { serviceNft, minter, agent } = base; - const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); - const maturity = 100; + const { deployer, contributor1, ipVault, founder } = await getAccounts(); + const { minter, agent } = base; const agentToken = await ethers.getContractAt("AgentToken", agent.token); await minter.connect(deployer).setIPShare(5000); @@ -645,82 +602,165 @@ describe("Contribution", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test model", base, contributor1.address, - [founder] + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const balance = await agentToken.balanceOf(ipVault.address); - expect(balance).to.equal(parseEther("50")); + expect(balance).to.equal(parseEther("50000")); }); it("should be able to adjust the IP share per agent", async function () { - const base = await loadFixture(deployWithAgent); - const { ipVault, contributor1, contributor2, founder, agentVeToken } = - await getAccounts(); - const { contributionNft, serviceNft, minter, agent } = base; - const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); - const maturity = 100; - const agentToken = await ethers.getContractAt("AgentToken", agent.token); - - await veToken.connect(founder).delegate(founder.address); - - const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); - - const calldata = await getSetIPShareCalldata( - minter, - agent.virtualId, - 1000n - ); - - await agentDAO.propose( - [minter.target], - [0], - [calldata], - "Set agent token multiplier" - ); - const filter = agentDAO.filters.ProposalCreated; - const events = await agentDAO.queryFilter(filter, -1); - const event = events[0]; - const proposalId = event.args[0]; - - await mine(1); - await agentDAO.connect(founder).castVote(proposalId, 1); - - // No agent token minted for dataset contribution - const c1 = await createContribution( - 1, - 0, - maturity, - 0, - false, - 0, - "Dataset", - base, - contributor1.address, - [founder] - ); - - const c2 = await createContribution( - 1, - 0, - maturity, - 0, - true, - c1, - "Test model", - base, - contributor2.address, - [founder] - ); - - const balance = await agentToken.balanceOf(ipVault.address); - expect(balance).to.equal(parseEther("10")); - + const base = await loadFixture(deployWithAgent); + const { ipVault, contributor1, contributor2, founder } = + await getAccounts(); + const { minter, agent } = base; + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + + await veToken.connect(founder).delegate(founder.address); + + const agentDAO = await ethers.getContractAt("AgentDAO", agent.dao); + + const calldata = await getSetIPShareCalldata( + minter, + agent.virtualId, + 1000n + ); + + await agentDAO.propose( + [minter.target], + [0], + [calldata], + "Set agent token multiplier" + ); + const filter = agentDAO.filters.ProposalCreated; + const events = await agentDAO.queryFilter(filter, -1); + const event = events[0]; + const proposalId = event.args[0]; + + await mine(1); + await agentDAO.connect(founder).castVote(proposalId, 1); + + // No agent token minted for dataset contribution + const c1 = await createContribution( + 1, + 0, + 0, + false, + 0, + "Dataset", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + + await createContribution( + 1, + 0, + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + + const balance = await agentToken.balanceOf(ipVault.address); + expect(balance).to.equal(parseEther("10000")); + }); + + it("should get max 215 elo rating for one sided votes", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1, contributor2, founder } = await getAccounts(); + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token + ); + + // First contribution always gets 100 impact + await createContribution( + 1, + 0, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + + expect(await agentToken.balanceOf(contributor2.address)).to.equal(0); + // Second contribution gets 215 impact + const nftId = await createContribution( + 1, + 0, + 0, + true, + 0, + "Test 2", + base, + contributor2.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + expect(await base.serviceNft.getImpact(nftId)).to.be.equal(215n); + expect(await base.serviceNft.getMaturity(nftId)).to.be.equal(315n); + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("215000")); + }); + + it("should calculate correct elo rating", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1, contributor2, founder } = await getAccounts(); + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token + ); + + // First contribution always gets 100 impact + await createContribution( + 1, + 0, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + + expect(await agentToken.balanceOf(contributor2.address)).to.equal(0); + // Second contribution gets 215 impact + const nftId = await createContribution( + 1, + 0, + 0, + true, + 0, + "Test 2", + base, + contributor2.address, + [founder], + [1, 1, 1, 1, 1, 2, 0, 3, 1, 1] + ); + + expect(await base.serviceNft.getImpact(nftId)).to.be.equal(142n); + expect(await base.serviceNft.getMaturity(nftId)).to.be.equal(242n); + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("142000")); }); }); diff --git a/test/delegate.js b/test/delegate.js index 2944abc..cbf80bb 100644 --- a/test/delegate.js +++ b/test/delegate.js @@ -1,25 +1,15 @@ /* Test delegation with history */ -const { parseEther, toBeHex } = require("ethers/utils"); +const { parseEther } = require("ethers/utils"); const { expect } = require("chai"); const { loadFixture, mine, } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); -const getExecuteCallData = (factory, proposalId) => { - return factory.interface.encodeFunctionData("executeApplication", [ - proposalId, - false, - ]); -}; - describe("Delegation", function () { const PROPOSAL_THRESHOLD = parseEther("100000"); - const QUORUM = parseEther("10000"); - const PROTOCOL_DAO_VOTING_PERIOD = 300; - const MATURITY_SCORE = toBeHex(2000, 32); // 20% const genesisInput = { name: "Jessica", @@ -90,7 +80,7 @@ describe("Delegation", function () { ); await agentFactory.waitForDeployment(); await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); - const minter = await ethers.deployContract("Minter", [ + const minter = await upgrades.deployProxy(await ethers.getContractFactory("Minter"), [ service.target, contribution.target, agentNft.target, @@ -99,16 +89,20 @@ describe("Delegation", function () { ipVault.address, agentFactory.target, deployer.address, + process.env.MAX_IMPACT, ]); await minter.waitForDeployment(); - await agentFactory.setMinter(minter.target); await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); await agentFactory.setTokenAdmin(deployer.address); await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LP_SUPPLY, + process.env.AGENT_TOKEN_VAULT_SUPPLY, process.env.AGENT_TOKEN_LIMIT, process.env.AGENT_TOKEN_LIMIT, - process.env.BOT_PROTECTION + process.env.BOT_PROTECTION, + minter.target ); await agentFactory.setTokenTaxParams( process.env.TAX, @@ -116,7 +110,6 @@ describe("Delegation", function () { process.env.SWAP_THRESHOLD, treasury.address ); - await agentFactory.setDefaultDelegatee(deployer.address); return { virtualToken, agentFactory, agentNft }; } @@ -204,15 +197,9 @@ describe("Delegation", function () { mine(1); const block3 = await ethers.provider.getBlockNumber(); - expect( - await veToken.getPastDelegates(account1, block2) - ).to.equal(account2); - expect( - await veToken.getPastDelegates(account1, block3) - ).to.equal(account3); - expect( - await veToken.getPastDelegates(account1, block1) - ).to.equal(account1); + expect(await veToken.getPastDelegates(account1, block2)).to.equal(account2); + expect(await veToken.getPastDelegates(account1, block3)).to.equal(account3); + expect(await veToken.getPastDelegates(account1, block1)).to.equal(account1); expect(await veToken.delegates(account1)).to.equal(account3); }); @@ -228,10 +215,7 @@ describe("Delegation", function () { await mine(1); for (let i = 0; i < 8; i++) { expect( - await veToken.getPastDelegates( - account1, - blockNumber + i + 1 - ) + await veToken.getPastDelegates(account1, blockNumber + i + 1) ).to.equal(this.accounts[i]); } }); diff --git a/test/deprecated/contribution.js b/test/deprecated/contribution.js.old similarity index 100% rename from test/deprecated/contribution.js rename to test/deprecated/contribution.js.old diff --git a/test/deprecated/genesisDAO.js b/test/deprecated/genesisDAO.js.old similarity index 100% rename from test/deprecated/genesisDAO.js rename to test/deprecated/genesisDAO.js.old diff --git a/test/deprecated/rewards.js b/test/deprecated/rewards.js.old similarity index 100% rename from test/deprecated/rewards.js rename to test/deprecated/rewards.js.old diff --git a/test/deprecated/staking.js b/test/deprecated/staking.js.old similarity index 100% rename from test/deprecated/staking.js rename to test/deprecated/staking.js.old diff --git a/test/elo.js b/test/elo.js new file mode 100644 index 0000000..c50ab91 --- /dev/null +++ b/test/elo.js @@ -0,0 +1,22 @@ +/* +Test delegation with history +*/ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +describe("Elo Rating", function () { + before(async function () { + const signers = await ethers.getSigners(); + this.accounts = signers.map((signer) => signer.address); + this.signers = signers; + }); + + it("should calculate correct elo", async function () { + const [deployer] = await ethers.getSigners(); + const Contract = await ethers.getContractFactory("EloCalculator"); + const calculator = await upgrades.deployProxy(Contract, [deployer.address]); + + const res = await calculator.battleElo(100, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + expect(res).to.be.equal(315n); + }); +}); diff --git a/test/rewardsV2.js b/test/rewardsV2.js index 3f13ca8..cbf4578 100644 --- a/test/rewardsV2.js +++ b/test/rewardsV2.js @@ -74,8 +74,14 @@ describe("RewardsV2", function () { ); await virtualToken.waitForDeployment(); + const EloCalculator = await ethers.getContractFactory("EloCalculator"); + const eloCalculator = await upgrades.deployProxy(EloCalculator, [ + deployer.address, + ]); + const AgentNft = await ethers.getContractFactory("AgentNftV2"); const agentNft = await upgrades.deployProxy(AgentNft, [deployer.address]); + await agentNft.connect(deployer).setEloCalculator(eloCalculator.target); const contribution = await upgrades.deployProxy( await ethers.getContractFactory("ContributionNft"), @@ -114,25 +120,32 @@ describe("RewardsV2", function () { ); await agentFactory.waitForDeployment(); await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); - const minter = await ethers.deployContract("Minter", [ - service.target, - contribution.target, - agentNft.target, - process.env.IP_SHARES, - process.env.IMPACT_MULTIPLIER, - ipVault.address, - agentFactory.target, - deployer.address, - ]); + const minter = await upgrades.deployProxy( + await ethers.getContractFactory("Minter"), + [ + service.target, + contribution.target, + agentNft.target, + process.env.IP_SHARES, + process.env.IMPACT_MULTIPLIER, + ipVault.address, + agentFactory.target, + deployer.address, + process.env.MAX_IMPACT, + ] + ); await minter.waitForDeployment(); - await agentFactory.setMinter(minter.target); await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); await agentFactory.setTokenAdmin(deployer.address); await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LP_SUPPLY, + process.env.AGENT_TOKEN_VAULT_SUPPLY, process.env.AGENT_TOKEN_LIMIT, process.env.AGENT_TOKEN_LIMIT, - process.env.BOT_PROTECTION + process.env.BOT_PROTECTION, + minter.target ); await agentFactory.setTokenTaxParams( process.env.TAX, @@ -159,7 +172,7 @@ describe("RewardsV2", function () { ); await rewards.waitForDeployment(); await rewards.grantRole(await rewards.GOV_ROLE(), deployer.address); - + await mine(1); return { virtualToken, agentFactory, @@ -247,14 +260,14 @@ describe("RewardsV2", function () { async function createContribution( virtualId, coreId, - maturity, parentId, isModel, datasetId, desc, base, account, - voters + voters, + votes ) { const { serviceNft, contributionNft, minter, agentNft } = base; const daoAddr = (await agentNft.virtualInfo(virtualId)).dao; @@ -286,9 +299,7 @@ describe("RewardsV2", function () { isModel, datasetId ); - const voteParams = isModel - ? abi.encode(["uint256", "uint8[] memory"], [maturity, [0, 1, 1, 0, 2]]) - : "0x"; + const voteParams = isModel ? abi.encode(["uint8[] memory"], [votes]) : "0x"; for (const voter of voters) { await agentDAO @@ -309,7 +320,6 @@ describe("RewardsV2", function () { const base = await loadFixture(deployWithAgent); const { rewards, virtualToken, agent } = base; const { contributor1, founder, validator1 } = await getAccounts(); - const maturity = 100; // Founder should delegate to another person for us to test the different set of rewards const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); await veToken.connect(founder).delegate(validator1.address); @@ -318,14 +328,14 @@ describe("RewardsV2", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -341,23 +351,21 @@ describe("RewardsV2", function () { const base = await loadFixture(deployWithAgent); const { rewards, virtualToken, agent } = base; const { contributor1, founder, validator1 } = await getAccounts(); - const maturity = 100; // Founder should delegate to another person for us to test the different set of rewards const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); await veToken.connect(founder).delegate(validator1.address); await mine(1); - await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -385,26 +393,17 @@ describe("RewardsV2", function () { await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) ); const validatorVClaimable = formatEther( - await rewards.getTotalClaimableValidatorRewards( - validator1.address, - [1] - ) + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) ); expect(validatorSClaimable).to.equal("0.0"); expect(validatorVClaimable).to.equal("10000.0"); // Nothing for contributor const conributorSClaimable = formatEther( - await rewards.getTotalClaimableStakerRewards( - contributor1.address, - [1] - ) + await rewards.getTotalClaimableStakerRewards(contributor1.address, [1]) ); const contributorVClaimable = formatEther( - await rewards.getTotalClaimableValidatorRewards( - contributor1.address, - [1] - ) + await rewards.getTotalClaimableValidatorRewards(contributor1.address, [1]) ); expect(conributorSClaimable).to.equal("0.0"); expect(contributorVClaimable).to.equal("0.0"); @@ -414,7 +413,6 @@ describe("RewardsV2", function () { const base = await loadFixture(deployWithAgent); const { rewards, virtualToken, agent } = base; const { contributor1, founder, validator1 } = await getAccounts(); - const maturity = 100; // Founder should delegate to another person for us to test the different set of rewards const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); await veToken.connect(founder).delegate(validator1.address); @@ -423,14 +421,14 @@ describe("RewardsV2", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -459,26 +457,17 @@ describe("RewardsV2", function () { await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) ); const validatorVClaimable = formatEther( - await rewards.getTotalClaimableValidatorRewards( - validator1.address, - [1] - ) + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) ); expect(validatorSClaimable).to.equal("0.0"); expect(validatorVClaimable).to.equal("9000.0"); // Nothing for contributor const conributorSClaimable = formatEther( - await rewards.getTotalClaimableStakerRewards( - contributor1.address, - [1] - ) + await rewards.getTotalClaimableStakerRewards(contributor1.address, [1]) ); const contributorVClaimable = formatEther( - await rewards.getTotalClaimableValidatorRewards( - contributor1.address, - [1] - ) + await rewards.getTotalClaimableValidatorRewards(contributor1.address, [1]) ); expect(conributorSClaimable).to.equal("0.0"); expect(contributorVClaimable).to.equal("0.0"); @@ -486,15 +475,8 @@ describe("RewardsV2", function () { it("should be able to distribute protocol emission for multiple virtuals with arbitrary LP values", async function () { const base = await loadFixture(deployWithAgent); - const { agent, agentNft, virtualToken, rewards } = base; - const { - contributor1, - contributor2, - validator1, - validator2, - founder, - trader, - } = await getAccounts(); + const { agentNft, virtualToken, rewards } = base; + const { contributor1, validator1, founder, trader } = await getAccounts(); // Create 3 more virtuals to sum up to 4 const app2 = await createApplication(base, founder, 1); const agent2 = await createAgent(base, app2); @@ -516,14 +498,14 @@ describe("RewardsV2", function () { await createContribution( i, 0, - 100, 0, true, 0, `Test ${i}`, base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); } @@ -593,7 +575,6 @@ describe("RewardsV2", function () { const { rewards, virtualToken, agent, agentNft } = base; const { contributor1, founder, validator1, trader, validator2 } = await getAccounts(); - const maturity = 100; // Founder should delegate to another person for us to test the different set of rewards const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); await veToken.connect(founder).delegate(validator1.address); @@ -628,14 +609,14 @@ describe("RewardsV2", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -693,7 +674,6 @@ describe("RewardsV2", function () { const { rewards, virtualToken, agent, agentNft } = base; const { contributor1, founder, validator1, trader, validator2 } = await getAccounts(); - const maturity = 100; const agentToken = await ethers.getContractAt("AgentToken", agent.token); const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); @@ -706,13 +686,14 @@ describe("RewardsV2", function () { process.env.UNISWAP_ROUTER ); - await virtualToken.mint(trader.address, parseEther("100000")); + // Trader buys tokens + await virtualToken.mint(trader.address, parseEther("300000")); await virtualToken .connect(trader) - .approve(router.target, parseEther("100000")); + .approve(router.target, parseEther("300000")); const agentTokenAddr = (await agentNft.virtualInfo(1)).token; - const amountToBuy = parseEther("40000"); - const capital = parseEther("100000"); + const amountToBuy = parseEther("10000000"); + const capital = parseEther("300000"); await router .connect(trader) .swapTokensForExactTokens( @@ -726,6 +707,8 @@ describe("RewardsV2", function () { await agentToken .connect(trader) .approve(router.target, await agentToken.balanceOf(trader.address)); + + // Add liquidity await router .connect(trader) .addLiquidity( @@ -739,24 +722,31 @@ describe("RewardsV2", function () { Math.floor(new Date().getTime() / 1000 + 600000) ); await mine(1); - await lp.connect(trader).approve(veToken.target, await lp.balanceOf(trader.address)); + // Stake + await lp + .connect(trader) + .approve(veToken.target, await lp.balanceOf(trader.address)); await veToken .connect(trader) - .stake(await lp.balanceOf(trader.address), trader.address, validator2.address); + .stake( + await lp.balanceOf(trader.address), + trader.address, + validator2.address + ); await mine(1); // Validator 1 voting await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1, validator2] + [validator1, validator2], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -770,6 +760,11 @@ describe("RewardsV2", function () { ); await mine(1); + const v1 = parseFloat( + formatEther(await veToken.balanceOf(founder.address)) + ); + const v2 = parseFloat(formatEther(await veToken.balanceOf(trader.address))); + // Staker1 + Validator 1 const staker1SClaimable = formatEther( await rewards.getTotalClaimableStakerRewards(founder.address, [1]) @@ -777,7 +772,8 @@ describe("RewardsV2", function () { const staker1VClaimable = formatEther( await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) ); - expect(parseFloat(staker1SClaimable)).to.be.greaterThan(70000); + + expect(parseFloat(staker1SClaimable).toFixed(4)).to.be.equal("72964.6543"); expect(staker1VClaimable).to.equal("0.0"); const validator1SClaimable = formatEther( @@ -787,16 +783,19 @@ describe("RewardsV2", function () { await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) ); expect(validator1SClaimable).to.equal("0.0"); - expect(parseFloat(validator1VClaimable)).to.be.greaterThan(7000); + expect(parseFloat(validator1VClaimable).toFixed(4)).to.be.equal( + "8107.1838" + ); // Staker2 + Validator 2 const staker2SClaimable = formatEther( await rewards.getTotalClaimableStakerRewards(trader.address, [1]) ); + const staker2VClaimable = formatEther( await rewards.getTotalClaimableValidatorRewards(trader.address, [1]) ); - expect(parseFloat(staker2SClaimable)).to.be.greaterThan(10000); + expect(parseFloat(staker2SClaimable).toFixed(4)).to.be.equal("17035.3457"); expect(staker2VClaimable).to.equal("0.0"); const validator2SClaimable = formatEther( @@ -806,7 +805,9 @@ describe("RewardsV2", function () { await rewards.getTotalClaimableValidatorRewards(validator2.address, [1]) ); expect(validator2SClaimable).to.equal("0.0"); - expect(parseFloat(validator2VClaimable)).to.be.greaterThan(1000); + expect(parseFloat(validator2VClaimable).toFixed(4)).to.be.equal( + "1892.8162" + ); }); it("should be able to distribute protocol emission for single virtual", async function () { @@ -822,14 +823,14 @@ describe("RewardsV2", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -845,7 +846,7 @@ describe("RewardsV2", function () { const base = await loadFixture(deployWithAgent); const { rewards, virtualToken, agent } = base; const { contributor1, founder, validator1 } = await getAccounts(); - const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); await veToken.connect(founder).delegate(validator1.address); @@ -854,14 +855,14 @@ describe("RewardsV2", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -889,26 +890,17 @@ describe("RewardsV2", function () { await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) ); const validatorVClaimable = formatEther( - await rewards.getTotalClaimableValidatorRewards( - validator1.address, - [1] - ) + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) ); expect(validatorSClaimable).to.equal("0.0"); expect(validatorVClaimable).to.equal("10000.0"); // Nothing for contributor const conributorSClaimable = formatEther( - await rewards.getTotalClaimableStakerRewards( - contributor1.address, - [1] - ) + await rewards.getTotalClaimableStakerRewards(contributor1.address, [1]) ); const contributorVClaimable = formatEther( - await rewards.getTotalClaimableValidatorRewards( - contributor1.address, - [1] - ) + await rewards.getTotalClaimableValidatorRewards(contributor1.address, [1]) ); expect(conributorSClaimable).to.equal("0.0"); expect(contributorVClaimable).to.equal("0.0"); @@ -918,7 +910,7 @@ describe("RewardsV2", function () { const base = await loadFixture(deployWithAgent); const { rewards, virtualToken, agent } = base; const { contributor1, founder, validator1 } = await getAccounts(); - const maturity = 100; + // Founder should delegate to another person for us to test the different set of rewards const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); await veToken.connect(founder).delegate(validator1.address); @@ -927,14 +919,14 @@ describe("RewardsV2", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -963,26 +955,17 @@ describe("RewardsV2", function () { await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) ); const validatorVClaimable = formatEther( - await rewards.getTotalClaimableValidatorRewards( - validator1.address, - [1] - ) + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) ); expect(validatorSClaimable).to.equal("0.0"); expect(validatorVClaimable).to.equal("9000.0"); // Nothing for contributor const conributorSClaimable = formatEther( - await rewards.getTotalClaimableStakerRewards( - contributor1.address, - [1] - ) + await rewards.getTotalClaimableStakerRewards(contributor1.address, [1]) ); const contributorVClaimable = formatEther( - await rewards.getTotalClaimableValidatorRewards( - contributor1.address, - [1] - ) + await rewards.getTotalClaimableValidatorRewards(contributor1.address, [1]) ); expect(conributorSClaimable).to.equal("0.0"); expect(contributorVClaimable).to.equal("0.0"); @@ -990,22 +973,15 @@ describe("RewardsV2", function () { it("should be able to distribute protocol emission for multiple virtuals with arbitrary LP values", async function () { const base = await loadFixture(deployWithAgent); - const { agent, agentNft, virtualToken, rewards } = base; - const { - contributor1, - contributor2, - validator1, - validator2, - founder, - trader, - } = await getAccounts(); + const { agentNft, virtualToken, rewards } = base; + const { validator1, founder, trader } = await getAccounts(); // Create 3 more virtuals to sum up to 4 const app2 = await createApplication(base, founder, 1); const agent2 = await createAgent(base, app2); const app3 = await createApplication(base, founder, 2); const agent3 = await createAgent(base, app3); const app4 = await createApplication(base, founder, 3); - const agent4 = await createAgent(base, app4); + const agent4 = await createAgent(base, app4); const lp3 = await rewards.getLPValue(agent3[0]); console.log("Initial LP ", lp3); @@ -1022,14 +998,14 @@ describe("RewardsV2", function () { await createContribution( i, 0, - 100, 0, true, 0, `Test ${i}`, base, trader.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); } @@ -1044,10 +1020,26 @@ describe("RewardsV2", function () { .approve(router.target, parseEther("400")); for (let i of [1, 3, 4]) { const agentTokenAddr = (await agentNft.virtualInfo(i)).token; - const agentToken = await ethers.getContractAt("AgentToken", agentTokenAddr); - await agentToken.connect(trader).approve(router.target, parseEther("400")) + const agentToken = await ethers.getContractAt( + "AgentToken", + agentTokenAddr + ); + await agentToken + .connect(trader) + .approve(router.target, parseEther("400")); const amountToAdd = parseEther((0.25 * i).toString()); const capital = parseEther("400"); + console.log( + "Adding liquidity for ", + i, + "AMount", + formatEther(amountToAdd), + " Balance ", + formatEther(await agentToken.balanceOf(trader.address)), + " Capital ", + formatEther(await virtualToken.balanceOf(trader.address)) + ); + await router .connect(trader) .addLiquidity( @@ -1055,10 +1047,13 @@ describe("RewardsV2", function () { agentTokenAddr, amountToAdd, amountToAdd, - amountToAdd, - amountToAdd, + 0, + 0, trader.address, - Math.floor(new Date().getTime() / 1000 + 6000000) + Math.floor(new Date().getTime() / 1000 + 6000000), + { + gasLimit: 1000000, + } ); const lp_after_buy = await rewards.getLPValue(i); console.log("lp after buy LP ", lp_after_buy); @@ -1067,22 +1062,20 @@ describe("RewardsV2", function () { // Distribute rewards // Expectations: - // virtual 4 = 100001000000000000000000n, - // virtual 3 = 100000750000000000000000n, - // virtual 1 = 100000250000000000000000n + // virtual 4 = 100000002000000000000000n, + // virtual 3 = 100000001500000000000000n, + // virtual 1 = 100000000500000000000000n // virtual 2 = 0 // Using gwei as calculation size due to BigInt limitations const rewardSize = 300000; - const Lp4 = BigInt(100001000000000); - const Lp3 = BigInt(100000750000000); - const Lp1 = BigInt(100000250000000); - const rewardSizeInGwei = BigInt(rewardSize * 10 ** 9) - - - const totalLp = Lp1 + Lp3 + Lp4 - console.log("totallp", totalLp) + const Lp4 = BigInt(100000002000000000000000n); + const Lp3 = BigInt(100000001500000000000000n); + const Lp1 = BigInt(100000000500000000000000n); + const rewardSizeInGwei = BigInt(rewardSize * 10 ** 9); + const totalLp = Lp1 + Lp3 + Lp4; + console.log("totallp", totalLp); await virtualToken.approve( rewards.target, @@ -1098,10 +1091,12 @@ describe("RewardsV2", function () { founder.address, [1] ); - + //99999583336111092592716 - const rewardSizeAfterProtocol = rewardSizeInGwei * BigInt(process.env.STAKER_SHARES) / BigInt(10000) - const expectedRewardLP1Ratio = await (rewardSizeAfterProtocol * Lp1) / totalLp + const rewardSizeAfterProtocol = + (rewardSizeInGwei * BigInt(process.env.STAKER_SHARES)) / BigInt(10000); + const expectedRewardLP1Ratio = + (await (rewardSizeAfterProtocol * Lp1)) / totalLp; const rewards2 = await rewards.getTotalClaimableStakerRewards( founder.address, @@ -1111,20 +1106,24 @@ describe("RewardsV2", function () { founder.address, [3] ); - const expectedRewardLP3Ratio = await (rewardSizeAfterProtocol * Lp3) / totalLp - + const expectedRewardLP3Ratio = + (await (rewardSizeAfterProtocol * Lp3)) / totalLp; const rewards4 = await rewards.getTotalClaimableStakerRewards( founder.address, [4] ); - const expectedRewardLP4Ratio = (rewardSizeAfterProtocol * Lp4) / totalLp - - - expect(await parseInt(ethers.formatUnits(rewards1.toString(), "gwei"))).to.be.equal(expectedRewardLP1Ratio); - expect(await parseInt(ethers.formatUnits(rewards3.toString(), "gwei"))).to.be.equal(expectedRewardLP3Ratio); - expect(await parseInt(ethers.formatUnits(rewards4.toString(), "gwei"))).to.be.equal(expectedRewardLP4Ratio); + const expectedRewardLP4Ratio = (rewardSizeAfterProtocol * Lp4) / totalLp; + expect( + await parseInt(ethers.formatUnits(rewards1.toString(), "gwei")) + ).to.be.equal(expectedRewardLP1Ratio); + expect( + await parseInt(ethers.formatUnits(rewards3.toString(), "gwei")) + ).to.be.equal(expectedRewardLP3Ratio); + expect( + await parseInt(ethers.formatUnits(rewards4.toString(), "gwei")) + ).to.be.equal(expectedRewardLP4Ratio); expect(rewards4).to.be.greaterThan(rewards3); expect(rewards3).to.be.greaterThan(rewards1); @@ -1136,7 +1135,6 @@ describe("RewardsV2", function () { const { rewards, virtualToken, agent, agentNft } = base; const { contributor1, founder, validator1, trader, validator2 } = await getAccounts(); - const maturity = 100; // Founder should delegate to another person for us to test the different set of rewards const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); await veToken.connect(founder).delegate(validator1.address); @@ -1171,14 +1169,14 @@ describe("RewardsV2", function () { await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1] + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( @@ -1236,7 +1234,6 @@ describe("RewardsV2", function () { const { rewards, virtualToken, agent, agentNft } = base; const { contributor1, founder, validator1, trader, validator2 } = await getAccounts(); - const maturity = 100; const agentToken = await ethers.getContractAt("AgentToken", agent.token); const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); @@ -1249,13 +1246,13 @@ describe("RewardsV2", function () { process.env.UNISWAP_ROUTER ); - await virtualToken.mint(trader.address, parseEther("100000")); + await virtualToken.mint(trader.address, parseEther("200000")); await virtualToken .connect(trader) .approve(router.target, parseEther("100000")); const agentTokenAddr = (await agentNft.virtualInfo(1)).token; - const amountToBuy = parseEther("40000"); - const capital = parseEther("100000"); + const amountToBuy = parseEther("10000000"); // 20% of the pool + const capital = parseEther("200000"); await router .connect(trader) .swapTokensForExactTokens( @@ -1269,6 +1266,7 @@ describe("RewardsV2", function () { await agentToken .connect(trader) .approve(router.target, await agentToken.balanceOf(trader.address)); + await router .connect(trader) .addLiquidity( @@ -1282,24 +1280,30 @@ describe("RewardsV2", function () { Math.floor(new Date().getTime() / 1000 + 600000) ); await mine(1); - await lp.connect(trader).approve(veToken.target, await lp.balanceOf(trader.address)); + await lp + .connect(trader) + .approve(veToken.target, await lp.balanceOf(trader.address)); await veToken .connect(trader) - .stake(await lp.balanceOf(trader.address), trader.address, validator2.address); + .stake( + await lp.balanceOf(trader.address), + trader.address, + validator2.address + ); await mine(1); // Validator 1 voting await createContribution( 1, 0, - maturity, 0, true, 0, "Test", base, contributor1.address, - [validator1, validator2] + [validator1, validator2], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); const rewardSize = 100000; await virtualToken.approve( diff --git a/test/virtualGenesis.js b/test/virtualGenesis.js index 448c14e..b05b29e 100644 --- a/test/virtualGenesis.js +++ b/test/virtualGenesis.js @@ -21,7 +21,7 @@ const { } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); describe("AgentFactoryV2", function () { - const PROPOSAL_THRESHOLD = parseEther("100000"); //100k + const PROPOSAL_THRESHOLD = parseEther("50000"); // 50k const MATURITY_SCORE = toBeHex(2000, 32); // 20% const IP_SHARE = 1000; // 10% @@ -94,35 +94,42 @@ describe("AgentFactoryV2", function () { ); await agentFactory.waitForDeployment(); await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); - const minter = await ethers.deployContract("Minter", [ - service.target, - contribution.target, - agentNft.target, - process.env.IP_SHARES, - process.env.IMPACT_MULTIPLIER, - ipVault.address, - agentFactory.target, - deployer.address, - ]); + const minter = await upgrades.deployProxy( + await ethers.getContractFactory("Minter"), + [ + service.target, + contribution.target, + agentNft.target, + process.env.IP_SHARES, + process.env.IMPACT_MULTIPLIER, + ipVault.address, + agentFactory.target, + deployer.address, + process.env.MAX_IMPACT, + ] + ); await minter.waitForDeployment(); - await agentFactory.setMinter(minter.target); await agentFactory.setMaturityDuration(86400 * 365 * 10); // 10years await agentFactory.setUniswapRouter(process.env.UNISWAP_ROUTER); await agentFactory.setTokenAdmin(deployer.address); await agentFactory.setTokenSupplyParams( + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LP_SUPPLY, + process.env.AGENT_TOKEN_VAULT_SUPPLY, process.env.AGENT_TOKEN_LIMIT, process.env.AGENT_TOKEN_LIMIT, - process.env.BOT_PROTECTION + process.env.BOT_PROTECTION, + minter.target ); + await agentFactory.setTokenTaxParams( process.env.TAX, process.env.TAX, process.env.SWAP_THRESHOLD, treasury.address ); - await agentFactory.setDefaultDelegatee(deployer.address); - return { virtualToken, agentFactory, agentNft }; + return { virtualToken, agentFactory, agentNft, minter }; } async function deployWithApplication() { @@ -261,14 +268,20 @@ describe("AgentFactoryV2", function () { }); it("agent component C1: Agent Token", async function () { - const { agent } = await loadFixture(deployWithAgent); + const { agent, minter } = await loadFixture(deployWithAgent); const agentToken = await ethers.getContractAt("AgentToken", agent.token); - expect(await agentToken.totalSupply()).to.be.equal(PROPOSAL_THRESHOLD); + expect(await agentToken.totalSupply()).to.be.equal( + parseEther(process.env.AGENT_TOKEN_LIMIT) + ); + expect(await agentToken.balanceOf(minter.target)).to.be.equal( + parseEther(process.env.AGENT_TOKEN_VAULT_SUPPLY) + ); }); it("agent component C2: LP Pool", async function () { const { agent, virtualToken } = await loadFixture(deployWithAgent); const lp = await ethers.getContractAt("IUniswapV2Pair", agent.lp); + const t0 = await lp.token0(); const t1 = await lp.token1(); @@ -276,25 +289,34 @@ describe("AgentFactoryV2", function () { expect(addresses).contain(t0); expect(addresses).contain(t1); + // t0 and t1 will change position dynamically const reserves = await lp.getReserves(); - expect(reserves[0]).to.be.equal(PROPOSAL_THRESHOLD); - expect(reserves[1]).to.be.equal(PROPOSAL_THRESHOLD); + expect(reserves[0]).to.be.equal( + t0 === agent.token + ? parseEther(process.env.AGENT_TOKEN_LP_SUPPLY) + : PROPOSAL_THRESHOLD + ); + expect(reserves[1]).to.be.equal( + t1 === agent.token + ? parseEther(process.env.AGENT_TOKEN_LP_SUPPLY) + : PROPOSAL_THRESHOLD + ); }); it("agent component C3: Agent veToken", async function () { const { agent } = await loadFixture(deployWithAgent); - const { founder, deployer } = await getAccounts(); + const { founder } = await getAccounts(); + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); - const balance = await veToken.balanceOf(founder.address); - expect(parseFloat(formatEther(balance).toString()).toFixed(2)).to.be.equal( - "100000.00" - ); - // Delegate to default delegatee - expect( - parseFloat( - formatEther(await veToken.getVotes(deployer.address)).toString() - ).toFixed(2) - ).to.be.equal("100000.00"); + const balance = parseFloat( + formatEther(await veToken.balanceOf(founder.address)) + ).toFixed(2); + const lp = await ethers.getContractAt("ERC20", agent.lp); + const votes = parseFloat( + formatEther(await veToken.getVotes(founder.address)) + ).toFixed(2); + expect(balance).to.equal("1581138.83"); + expect(votes).to.equal("1581138.83"); }); it("agent component C4: Agent DAO", async function () { @@ -353,9 +375,11 @@ describe("AgentFactoryV2", function () { deployWithAgent ); const agentToken = await ethers.getContractAt("AgentToken", agent.token); - expect(await agentToken.totalSupply()).to.be.equal(PROPOSAL_THRESHOLD); + expect(await agentToken.totalSupply()).to.be.equal( + parseEther(process.env.AGENT_TOKEN_LIMIT) + ); expect(await agentToken.balanceOf(agent.lp)).to.be.equal( - PROPOSAL_THRESHOLD + parseEther(process.env.AGENT_TOKEN_LP_SUPPLY) ); }); @@ -372,8 +396,8 @@ describe("AgentFactoryV2", function () { const agentToken = await ethers.getContractAt("AgentToken", agent.token); // Buy tokens - const amountToBuy = parseEther("90"); - const capital = parseEther("200"); + const amountToBuy = parseEther("10000000"); + const capital = parseEther("200000000"); await virtualToken.mint(trader.address, capital); await virtualToken .connect(trader) @@ -416,9 +440,11 @@ describe("AgentFactoryV2", function () { ///////////////// // Staking, and able to delegate to anyone await lpToken.connect(trader).approve(agent.veToken, parseEther("10")); - await veToken - .connect(trader) - .stake(parseEther("10"), trader.address, poorMan.address); + await expect( + veToken + .connect(trader) + .stake(parseEther("10"), trader.address, poorMan.address) + ).to.be.not.reverted; }); it("should deny staking on private agent", async function () { @@ -542,8 +568,8 @@ describe("AgentFactoryV2", function () { // Get trader to stake on poorMan so that we have 2 validators // Buy tokens - const amountToBuy = parseEther("90"); - const capital = parseEther("200"); + const amountToBuy = parseEther("1000000"); + const capital = parseEther("200000"); await virtualToken.mint(trader.address, capital); await virtualToken .connect(trader) @@ -694,8 +720,8 @@ describe("AgentFactoryV2", function () { "IUniswapV2Router02", process.env.UNISWAP_ROUTER ); - const amountToBuy = parseEther("90"); - const capital = parseEther("200"); + const amountToBuy = parseEther("1000000"); + const capital = parseEther("2000000"); await virtualToken.mint(trader.address, capital); await virtualToken .connect(trader) From 197c72c60ab7507b9c17cd27a4f5ec93c0216719 Mon Sep 17 00:00:00 2001 From: kw Date: Wed, 19 Jun 2024 15:02:53 +0800 Subject: [PATCH 30/30] added migration function for agent nft --- contracts/virtualPersona/AgentNftV2.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contracts/virtualPersona/AgentNftV2.sol b/contracts/virtualPersona/AgentNftV2.sol index aeed359..93087c8 100644 --- a/contracts/virtualPersona/AgentNftV2.sol +++ b/contracts/virtualPersona/AgentNftV2.sol @@ -308,4 +308,20 @@ contract AgentNftV2 is function getEloCalculator() public view returns (address) { return _eloCalculator; } + + function migrateVirtual( + uint256 virtualId, + address dao, + address token, + address pool, + address veToken + ) public onlyRole(ADMIN_ROLE) { + VirtualInfo storage info = virtualInfos[virtualId]; + info.dao = dao; + info.token = token; + + VirtualLP storage lp = virtualLPs[virtualId]; + lp.pool = pool; + lp.veToken = veToken; + } }