diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json index d692914..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": { @@ -2204,7 +2219,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 +2423,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 +2435,7 @@ "label": "mapping(uint8 => string)", "numberOfBytes": "32" }, - "t_struct(VirtualInfo)11161_storage": { + "t_struct(VirtualInfo)11167_storage": { "label": "struct IAgentNft.VirtualInfo", "members": [ { @@ -6638,7 +6653,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 +6741,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 +6775,7 @@ "label": "bool", "numberOfBytes": "1" }, - "t_struct(InitializableStorage)10_storage": { + "t_struct(InitializableStorage)744_storage": { "label": "struct Initializable.InitializableStorage", "members": [ { @@ -6798,7 +6813,7 @@ "label": "bytes32", "numberOfBytes": "32" }, - "t_enum(ApplicationStatus)2249": { + "t_enum(ApplicationStatus)27694": { "label": "enum AgentFactory.ApplicationStatus", "members": [ "Active", @@ -6811,11 +6826,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 +6838,7 @@ "label": "string", "numberOfBytes": "32" }, - "t_struct(Application)2278_storage": { + "t_struct(Application)27723_storage": { "label": "struct AgentFactory.Application", "members": [ { @@ -6846,7 +6861,7 @@ }, { "label": "status", - "type": "t_enum(ApplicationStatus)2249", + "type": "t_enum(ApplicationStatus)27694", "offset": 0, "slot": "3" }, @@ -6907,7 +6922,7 @@ ], "numberOfBytes": "384" }, - "t_struct(RoleData)275_storage": { + "t_struct(RoleData)7540_storage": { "label": "struct AccessControl.RoleData", "members": [ { @@ -7442,6 +7457,4111 @@ ] } } + }, + "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)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": [ + { + "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": [ + "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/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/AgentReward.sol b/contracts/AgentReward.sol deleted file mode 100644 index dd3a7fc..0000000 --- a/contracts/AgentReward.sol +++ /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/AgentRewardV2.sol b/contracts/AgentRewardV2.sol index 97ce644..9bfd1c5 100644 --- a/contracts/AgentRewardV2.sol +++ b/contracts/AgentRewardV2.sol @@ -6,10 +6,13 @@ 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"; @@ -26,35 +29,24 @@ contract AgentRewardV2 is 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; // Rewards checkpoints, split into Master reward and Virtual shares - MainReward[] private _mainRewards; - mapping(uint256 virtualId => Reward[]) private _rewards; + Reward[] private _rewards; + mapping(uint256 virtualId => AgentReward[]) private _agentRewards; 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())) { @@ -75,17 +67,13 @@ contract AgentRewardV2 is 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; + _nextAgentRewardId = 1; } function getRewardSettings() @@ -106,26 +94,52 @@ contract AgentRewardV2 is return _rewardSettings.upperLookupRecent(timepoint); } - function getMainReward(uint32 pos) public view returns (MainReward memory) { - return _mainRewards[pos]; + function getReward(uint256 pos) public view returns (Reward memory) { + return _rewards[pos]; } - function getReward( + function getAgentReward( uint256 virtualId, - uint32 pos - ) public view returns (Reward memory) { - return _rewards[virtualId][pos]; + 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(uint256 virtualId) public view returns (uint256) { - return _rewards[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 // ---------------- - function distributeRewards(uint256 amount) public onlyGov returns (uint32) { + // 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( @@ -137,482 +151,271 @@ contract AgentRewardV2 is RewardSettingsCheckpoints.RewardSettings memory settings = getRewardSettings(); - uint256 protocolShares = _distributeProtocolRewards(amount); + uint256 protocolAmount = shouldShareWithProtocol + ? _distributeProtocolRewards(amount) + : 0; - uint256 agentShares = amount - protocolShares; - _prepareAgentsRewards(agentShares, settings); - return SafeCast.toUint32(_mainRewards.length - 1); - } + uint256 balance = amount - protocolAmount; + uint256 rewardIndex = _rewards.length; + uint virtualCount = virtualIds.length; + uint256[] memory lpValues = new uint256[](virtualCount); - function distributeRewardsForAgents( - uint32 mainRewardIndex, - uint256[] memory virtualIds - ) public onlyGov { - RewardSettingsCheckpoints.RewardSettings - memory settings = getRewardSettings(); + uint256 totalLPValues = 0; + for (uint i = 0; i < virtualCount; i++) { + lpValues[i] = getLPValue(virtualIds[i]); + totalLPValues += lpValues[i]; + } - for (uint256 i = 0; i < virtualIds.length; i++) { - _distributeAgentRewards(virtualIds[i], mainRewardIndex, settings); + if (totalLPValues <= 0) { + revert("Invalid LP values"); } - } - function _distributeProtocolRewards( - uint256 amount - ) private returns (uint256) { - RewardSettingsCheckpoints.RewardSettings - memory rewardSettings = _rewardSettings.latest(); - uint256 protocolShares = (amount * rewardSettings.protocolShares) / - DENOMINATOR; - protocolRewards += protocolShares; - return protocolShares; - } + _rewards.push(Reward(block.number, balance, lpValues, virtualIds)); - // 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 - }) + 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 ); } - - _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( + function _distributeAgentReward( uint256 virtualId, - uint32 mainRewardIndex, + uint256 rewardIndex, + uint256 amount, RewardSettingsCheckpoints.RewardSettings memory settings ) private { - if (_rewards[virtualId].length == 0) { - return; - } + uint256 agentRewardId = _nextAgentRewardId++; - MainReward memory mainReward = _mainRewards[mainRewardIndex]; + uint256 totalStaked = getTotalStaked(virtualId); - 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; + uint256 stakerAmount = (amount * settings.stakerShares) / DENOMINATOR; - _distributeValidatorRewards( - reward.validatorAmount, - virtualId, - reward.id, - reward.totalStaked - ); - _distributeContributorRewards( - reward.contributorAmount, - virtualId, - settings - ); + uint256 totalProposals = IAgentDAO( + IAgentNft(agentNft).virtualInfo(virtualId).dao + ).proposalCount(); - emit NewAgentReward( - mainRewardIndex, - virtualId, - reward.validatorAmount, - reward.contributorAmount, - reward.coreAmount + _agentRewards[virtualId].push( + AgentReward( + agentRewardId, + rewardIndex, + stakerAmount, + amount - stakerAmount, + totalProposals, + totalStaked + ) ); - } - // 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; - } + emit NewAgentReward(virtualId, agentRewardId); } - 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; - } + function _distributeProtocolRewards( + uint256 amount + ) private returns (uint256) { + RewardSettingsCheckpoints.RewardSettings + memory rewardSettings = _rewardSettings.latest(); + uint256 protocolShares = (amount * rewardSettings.protocolShares) / + DENOMINATOR; + protocolRewards += protocolShares; + return protocolShares; } // ---------------- - // Functions to query rewards + // Claim 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); + mapping(address account => mapping(uint256 virtualId => Claim claim)) _stakerClaims; + mapping(address account => mapping(uint256 virtualId => Claim claim)) _validatorClaims; - address delegatee = token.getPastDelegates( - account, - mainReward.blockNumber + 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) ); - - 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 + IAgentVeToken veToken = IAgentVeToken( + IAgentNft(agentNft).virtualLP(virtualId).veToken ); - uint256 votes = IERC5805(stakingAddress).getPastVotes( - delegatee, - mainReward.blockNumber + IAgentDAO dao = IAgentDAO( + IAgentNft(agentNft).virtualInfo(virtualId).dao ); - - 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 + 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 ); - } - - 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 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; - uint256 validatorGroupRewards = _validatorRewards[validator][reward.id]; - - return - (validatorGroupRewards * - (DENOMINATOR - uint256(rewardSettings.stakerShares))) / - DENOMINATOR; + totalClaimable += stakerReward; + } } - function _getClaimableValidatorRewards( - address validator, + function getClaimableValidatorRewards( + address account, 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); + ) 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; } - - 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); + 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; } - 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( + function getTotalClaimableValidatorRewards( address account, - uint256 virtualId - ) internal noReentrant { - uint256 amount = _getClaimableStakerRewards(account, virtualId); - if (amount == 0) { - return; + 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; } + } - 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 getAgentRewardCount( + uint256 virtualId + ) public view returns (uint256) { + return _agentRewards[virtualId].length; } - function _claimValidatorRewards(uint256 virtualId) internal noReentrant { + function claimStakerRewards( + uint256 virtualId + ) public noReentrant { address account = _msgSender(); + uint256 totalClaimable; + uint256 numRewards; + (totalClaimable, numRewards) = getClaimableStakerRewards( + account, + virtualId + ); - 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; - } + Claim storage claim = _stakerClaims[account][virtualId]; + claim.totalClaimed += totalClaimable; + claim.rewardCount = numRewards; - function withdrawValidatorPoolRewards(address recipient) external onlyGov { - require(validatorPoolRewards > 0, "No validator pool rewards"); - IERC20(rewardToken).safeTransfer(recipient, validatorPoolRewards); - validatorPoolRewards = 0; - } + IERC20(rewardToken).safeTransfer(account, totalClaimable); - function getServiceReward( - uint256 nftId - ) public view returns (ServiceReward memory) { - return _serviceRewards[nftId]; + emit StakerRewardClaimed(virtualId, account, numRewards, totalClaimable); } - function _claimServiceRewards(uint256 nftId) internal { + function claimValidatorRewards( + uint256 virtualId + ) public noReentrant { address account = _msgSender(); - require( - IERC721(contributionNft).ownerOf(nftId) == account, - "Not NFT owner" + uint256 totalClaimable; + uint256 numRewards; + (totalClaimable, numRewards) = getClaimableValidatorRewards( + account, + virtualId ); - ServiceReward storage serviceReward = _serviceRewards[nftId]; - uint256 total = (serviceReward.amount - serviceReward.totalClaimed); - - serviceReward.totalClaimed += total; + Claim storage claim = _validatorClaims[account][virtualId]; + claim.totalClaimed += totalClaimable; + claim.rewardCount = numRewards; - // Claim children rewards - uint256[] memory children = IContributionNft(contributionNft) - .getChildren(nftId); + IERC20(rewardToken).safeTransfer(account, totalClaimable); - uint256 totalChildrenAmount; - uint256 childAmount; - for (uint256 i = 0; i < children.length; i++) { - ServiceReward storage childReward = _serviceRewards[children[i]]; - - childAmount = (childReward.parentAmount - - childReward.totalClaimedParent); + emit ValidatorRewardClaimed(virtualId, account, totalClaimable); + } - if (childAmount > 0) { - childReward.totalClaimedParent += childAmount; - total += childAmount; - totalChildrenAmount += childAmount; - } - } + 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; - if (total == 0) { - return; + Claim storage claim = _stakerClaims[account][virtualId]; + claim.totalClaimed += claimable; + claim.rewardCount = numRewards; } - 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; + IERC20(rewardToken).safeTransfer(account, totalClaimable); } - function claimAllRewards( - uint256[] memory virtualIds, - uint256[] memory contributionNftIds - ) public { + function claimAllValidatorRewards( + uint256[] memory virtualIds + ) public noReentrant { address account = _msgSender(); - for (uint256 i = 0; i < virtualIds.length; i++) { - _claimStakerRewards(account, virtualIds[i]); - _claimValidatorRewards(virtualIds[i]); - } + 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; - for (uint256 i = 0; i < contributionNftIds.length; i++) { - _claimServiceRewards(contributionNftIds[i]); + Claim storage claim = _validatorClaims[account][virtualId]; + claim.totalClaimed += claimable; + claim.rewardCount = numRewards; } + + IERC20(rewardToken).safeTransfer(account, totalClaimable); } // ---------------- @@ -621,47 +424,26 @@ contract AgentRewardV2 is function setRewardSettings( uint16 protocolShares_, - uint16 contributorShares_, - uint16 stakerShares_, - uint16 parentShares_, - uint256 stakeThreshold_ + uint16 stakerShares_ ) public onlyGov { _rewardSettings.push( SafeCast.toUint32(block.number), RewardSettingsCheckpoints.RewardSettings( protocolShares_, - contributorShares_, - stakerShares_, - parentShares_, - stakeThreshold_ + stakerShares_ ) ); - emit RewardSettingsUpdated( - protocolShares_, - contributorShares_, - stakerShares_, - parentShares_, - stakeThreshold_ - ); + emit RewardSettingsUpdated(protocolShares_, stakerShares_); } function updateRefContracts( address rewardToken_, - address agentNft_, - address contributionNft_, - address serviceNft_ + address agentNft_ ) external onlyGov { rewardToken = rewardToken_; agentNft = agentNft_; - contributionNft = contributionNft_; - serviceNft = serviceNft_; - - emit RefContractsUpdated( - rewardToken_, - agentNft_, - contributionNft_, - serviceNft_ - ); + + 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/RewardTreasury.sol b/contracts/RewardTreasury.sol deleted file mode 100644 index e366ee7..0000000 --- a/contracts/RewardTreasury.sol +++ /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/contracts/contribution/ContributionNft.sol b/contracts/contribution/ContributionNft.sol index 7e799e1..c96cb55 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"; @@ -38,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(); @@ -181,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 1612a14..4741765 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,6 @@ 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); + function getEloCalculator() external view returns (address); } 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/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/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/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/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/contracts/token/IMinter.sol b/contracts/token/IMinter.sol new file mode 100644 index 0000000..1b14f5b --- /dev/null +++ b/contracts/token/IMinter.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +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 new file mode 100644 index 0000000..18419bd --- /dev/null +++ b/contracts/token/Minter.sol @@ -0,0 +1,206 @@ +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"; + +contract Minter is IMinter, Initializable, OwnableUpgradeable { + using SafeERC20 for IERC20; + + address public serviceNft; + address public contributionNft; + address public agentNft; + address public ipVault; + + uint256 public ipShare; // Share for IP holder + uint256 public impactMultiplier; + + uint256 public maxImpact; + + uint256 public constant DENOM = 10000; + + mapping(uint256 => bool) _mintedNfts; + + mapping(uint256 => uint256) public impactMulOverrides; + 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"); + locked = true; + _; + locked = false; + } + + modifier onlyAgentDAO(uint256 virtualId) { + address daoAddress = IAgentNft(agentNft).virtualInfo(virtualId).dao; + require(daoAddress == _msgSender(), "Only Agent DAO can operate"); + _; + } + + address agentFactory; + + /// @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, + uint256 maxImpact_ + ) public initializer { + __Ownable_init(initialOwner); + + serviceNft = serviceAddress; + contributionNft = contributionAddress; + agentNft = agentAddress; + ipShare = ipShare_; + impactMultiplier = impactMultiplier_; + ipVault = ipVault_; + agentFactory = agentFactory_; + maxImpact = maxImpact_; + } + + modifier onlyFactory() { + require(_msgSender() == agentFactory, "Caller is not Agent Factory"); + _; + } + + function setServiceNft(address serviceAddress) public onlyOwner { + serviceNft = serviceAddress; + } + + function setContributionNft(address contributionAddress) public onlyOwner { + contributionNft = contributionAddress; + } + + function setIPShare(uint256 _ipShare) public onlyOwner { + ipShare = _ipShare; + emit IPShareUpdated(_ipShare); + } + + function setIPShareOverride( + uint256 virtualId, + uint256 _ipShare + ) public onlyAgentDAO(virtualId) { + ipShareOverrides[virtualId] = _ipShare; + emit AgentIPShareUpdated(virtualId, _ipShare); + } + + function setIPVault(address _ipVault) public onlyOwner { + ipVault = _ipVault; + } + + function setAgentFactory(address _factory) public onlyOwner { + agentFactory = _factory; + } + + function setImpactMultiplier(uint256 _multiplier) public onlyOwner { + impactMultiplier = _multiplier; + emit ImpactMultiplierUpdated(_multiplier); + } + + function setImpactMulOverride( + uint256 virtualId, + uint256 mul + ) 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) { + uint256 mul = impactMulOverrides[virtualId]; + if (mul == 0) { + mul = impactMultiplier; + } + return mul; + } + + 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"); + + uint256 virtualId = IContributionNft(contributionNft).tokenVirtualId( + nftId + ); + require(virtualId != 0, "Agent not found"); + + _mintedNfts[nftId] = true; + + 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 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 * + 10 ** 18) / DENOM + : 0; + uint256 ipAmount = ((amount + dataAmount) * ipShare) / DENOM; + + // Mint to model owner + if (amount > 0) { + address modelOwner = IERC721(contributionNft).ownerOf(nftId); + IAgentToken(tokenAddress).transfer(modelOwner, amount); + } + + // Mint to Dataset owner + if (datasetId != 0) { + address datasetOwner = IERC721(contributionNft).ownerOf(datasetId); + IAgentToken(tokenAddress).transfer(datasetOwner, dataAmount); + } + + // To IP vault + if (ipAmount > 0) { + 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 0726438..cfa2192 100644 --- a/contracts/virtualPersona/AgentDAO.sol +++ b/contracts/virtualPersona/AgentDAO.sol @@ -1,10 +1,8 @@ // 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/utils/structs/Checkpoints.sol"; import "./IAgentDAO.sol"; @@ -12,10 +10,12 @@ 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, - GovernorUpgradeable, GovernorSettingsUpgradeable, GovernorCountingSimpleUpgradeable, GovernorStorageUpgradeable, @@ -31,6 +31,10 @@ contract AgentDAO is error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + uint256 private _totalScore; + + address private _agentNft; + constructor() { _disableInitializers(); } @@ -38,7 +42,7 @@ contract AgentDAO is function initialize( string memory name, IVotes token, - address contributionNft, + address agentNft, uint256 threshold, uint32 votingPeriod_ ) external initializer { @@ -50,7 +54,7 @@ contract AgentDAO is __GovernorCountingSimple_init(); __GovernorStorage_init(); - _contributionNft = contributionNft; + _agentNft = agentNft; } // The following functions are overrides required by Solidity. @@ -96,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, @@ -167,6 +172,7 @@ contract AgentDAO is ); if (!votedPreviously && hasVoted(proposalId, account)) { + ++_totalScore; _scores[account].push( SafeCast.toUint48(block.number), SafeCast.toUint208(scoreOf(account)) + 1 @@ -176,9 +182,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, @@ -186,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); @@ -224,4 +269,25 @@ 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; + } + + function totalScore() public view override returns (uint256) { + return _totalScore; + } } diff --git a/contracts/virtualPersona/AgentFactory.sol b/contracts/virtualPersona/AgentFactory.sol index fa9baa5..0dcbd9e 100644 --- a/contracts/virtualPersona/AgentFactory.sol +++ b/contracts/virtualPersona/AgentFactory.sol @@ -6,15 +6,22 @@ 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"; +import "./IAgentVeToken.sol"; import "./IAgentDAO.sol"; import "./IAgentNft.sol"; import "../libs/IERC6551Registry.sol"; -contract AgentFactory is Initializable, AccessControl { +contract AgentFactoryV2 is + IAgentFactory, + Initializable, + AccessControl, + PausableUpgradeable +{ using SafeERC20 for IERC20; uint256 private _nextId; @@ -27,16 +34,18 @@ contract AgentFactory is Initializable, AccessControl { address[] public allTokens; address[] public allDAOs; - address public assetToken; // Staked token - uint256 public maturityDuration; // Maturity duration in seconds + address public assetToken; // Base currency + uint256 public maturityDuration; // Staking duration in seconds for initial LP. eg: 10years - 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, address token, address dao, - address tba + address tba, + address veToken, + address lp ); event NewApplication(uint256 id); @@ -64,7 +73,7 @@ contract AgentFactory is Initializable, AccessControl { mapping(uint256 => Application) private _applications; - address public gov; + 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"); @@ -86,6 +95,23 @@ contract AgentFactory is Initializable, AccessControl { locked = false; } + /////////////////////////////////////////////////////////////// + // V2 Storage + /////////////////////////////////////////////////////////////// + address[] public allTradingTokens; + address private _uniswapRouter; + address public veTokenImplementation; + address private _minter; // Unused + address private _tokenAdmin; + address public defaultDelegatee; // Unused + + // Default agent token params + bytes private _tokenSupplyParams; + bytes private _tokenTaxParams; + uint16 private _tokenMultiplier; // Unused + + /////////////////////////////////////////////////////////////// + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -93,26 +119,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 gov_, address vault_ ) public initializer { + __Pausable_init(); + 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); - gov = gov_; _vault = vault_; } @@ -122,7 +147,7 @@ contract AgentFactory is Initializable, AccessControl { return _applications[proposalId]; } - function proposePersona( + function proposeAgent( string memory name, string memory symbol, string memory tokenURI, @@ -131,8 +156,8 @@ contract AgentFactory is Initializable, AccessControl { address tbaImplementation, uint32 daoVotingPeriod, uint256 daoThreshold - ) external returns (uint256) { - address sender = msg.sender; + ) public whenNotPaused returns (uint256) { + address sender = _msgSender(); require( IERC20(assetToken).balanceOf(sender) >= applicationThreshold, "Insufficient asset token" @@ -151,9 +176,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, @@ -194,50 +217,91 @@ 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 onlyGov { + function executeApplication(uint256 id, bool canStake) public noReentrant { + // This will bootstrap an Agent with following components: + // C1: Agent Token + // C2: LP Pool + Initial liquidity + // C3: Agent veToken + // C4: Agent DAO + // C5: Agent NFT + // C6: TBA + // C7: Stake liquidity token to get veToken require( _applications[id].status == ApplicationStatus.Active, "Application is not active" ); + require(_tokenAdmin != address(0), "Token admin not set"); + Application storage application = _applications[id]; + + require( + msg.sender == application.proposer || + hasRole(WITHDRAW_ROLE, msg.sender), + "Not proposer" + ); + + uint256 initialAmount = application.withdrawableAmount; + application.withdrawableAmount = 0; + application.status = ApplicationStatus.Executed; + + // C1 address token = _createNewAgentToken( application.name, - application.symbol, - application.proposer + application.symbol + ); + + // C2 + address lp = IAgentToken(token).liquidityPools()[0]; + IERC20(assetToken).transfer(token, initialAmount); + IAgentToken(token).addInitialLiquidity(address(this)); + + // C3 + address veToken = _createNewAgentVeToken( + string.concat("Staked ", application.name), + string.concat("s", application.symbol), + lp, + application.proposer, + canStake ); + + // C4 string memory daoName = string.concat(application.name, " DAO"); address payable dao = payable( _createNewDAO( daoName, - IVotes(token), + IVotes(veToken), application.daoVotingPeriod, application.daoThreshold ) ); - uint256 virtualId = IAgentNft(nft).mint( + + // C5 + uint256 virtualId = IAgentNft(nft).nextVirtualId(); + IAgentNft(nft).mint( + virtualId, _vault, application.tokenURI, dao, application.proposer, - application.cores + application.cores, + lp, + token ); + application.virtualId = virtualId; - IERC20(assetToken).forceApprove(token, application.withdrawableAmount); - IAgentToken(token).stake( - application.withdrawableAmount, - application.proposer, - application.proposer - ); + // C6 uint256 chainId; assembly { chainId := chainid() @@ -249,14 +313,17 @@ contract AgentFactory is Initializable, AccessControl { nft, virtualId ); - IAgentNft(nft).setTBA(virtualId, tbaAddress); - application.withdrawableAmount = 0; - application.status = ApplicationStatus.Executed; - application.virtualId = virtualId; + // C7 + IERC20(lp).approve(veToken, type(uint256).max); + IAgentVeToken(veToken).stake( + IERC20(lp).balanceOf(address(this)), + application.proposer, + application.proposer + ); - emit NewPersona(virtualId, token, dao, tbaAddress); + emit NewPersona(virtualId, token, dao, tbaAddress, veToken, lp); } function _createNewDAO( @@ -269,7 +336,7 @@ contract AgentFactory is Initializable, AccessControl { IAgentDAO(instance).initialize( name, token, - IAgentNft(nft).getContributionNft(), + nft, daoThreshold, daoVotingPeriod ); @@ -280,24 +347,43 @@ 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( + [_tokenAdmin, _uniswapRouter, assetToken], + abi.encode(name, symbol), + _tokenSupplyParams, + _tokenTaxParams + ); + + allTradingTokens.push(instance); + return instance; + } + + function _createNewAgentVeToken( + string memory name, + string memory symbol, + address stakingAsset, + address founder, + bool canStake + ) internal returns (address instance) { + instance = Clones.clone(veTokenImplementation); + IAgentVeToken(instance).initialize( name, symbol, founder, - assetToken, - nft, - block.timestamp + maturityDuration + stakingAsset, + block.timestamp + maturityDuration, + address(nft), + canStake ); allTokens.push(instance); return instance; } - function totalPersonas() public view returns (uint256) { + function totalAgents() public view returns (uint256) { return allTokens.length; } @@ -308,20 +394,101 @@ 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; } function setImplementations( address token, + address veToken, address dao ) public onlyRole(DEFAULT_ADMIN_ROLE) { tokenImplementation = token; daoImplementation = dao; + veTokenImplementation = veToken; + } + + function setMaturityDuration( + uint256 newDuration + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + maturityDuration = newDuration; + } + + function setUniswapRouter( + address router + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _uniswapRouter = router; + } + + function setTokenAdmin( + address newTokenAdmin + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _tokenAdmin = newTokenAdmin; + } + + function setTokenSupplyParams( + uint256 maxSupply, + uint256 lpSupply, + uint256 vaultSupply, + uint256 maxTokensPerWallet, + uint256 maxTokensPerTxn, + uint256 botProtectionDurationInSeconds, + address vault + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _tokenSupplyParams = abi.encode( + maxSupply, + lpSupply, + vaultSupply, + maxTokensPerWallet, + maxTokensPerTxn, + botProtectionDurationInSeconds, + vault + ); + } + + function setTokenTaxParams( + uint256 projectBuyTaxBasisPoints, + uint256 projectSellTaxBasisPoints, + uint256 taxSwapThresholdBasisPoints, + address projectTaxRecipient + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _tokenTaxParams = abi.encode( + projectBuyTaxBasisPoints, + projectSellTaxBasisPoints, + taxSwapThresholdBasisPoints, + projectTaxRecipient + ); + } + + function setAssetToken( + address newToken + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + assetToken = newToken; + } + + function pause() public onlyRole(DEFAULT_ADMIN_ROLE) { + _pause(); + } + + 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/AgentNft.sol b/contracts/virtualPersona/AgentNftV2.sol similarity index 76% rename from contracts/virtualPersona/AgentNft.sol rename to contracts/virtualPersona/AgentNftV2.sol index 05f3e04..93087c8 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, @@ -49,6 +49,14 @@ contract AgentNft is address private _contributionNft; address private _serviceNft; + // V2 Storage + 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); + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -66,6 +74,7 @@ contract AgentNft is __AccessControl_init(); _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); _grantRole(VALIDATOR_ADMIN_ROLE, defaultAdmin); + _grantRole(ADMIN_ROLE, defaultAdmin); _nextVirtualId = 1; } @@ -77,14 +86,21 @@ 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 + uint8[] memory coreTypes, + address pool, + address token ) external onlyRole(MINTER_ROLE) returns (uint256) { - uint256 virtualId = _nextVirtualId++; + _nextVirtualId++; _mint(to, virtualId); _setTokenURI(virtualId, newTokenURI); VirtualInfo storage info = virtualInfos[virtualId]; @@ -92,10 +108,14 @@ contract AgentNft is info.coreTypes = coreTypes; info.founder = founder; IERC5805 daoToken = GovernorVotes(theDAO).token(); - info.token = address(daoToken); + info.token = token; + + VirtualLP storage lp = virtualLPs[virtualId]; + lp.pool = pool; + lp.veToken = address(daoToken); + _stakingTokenToVirtualId[address(daoToken)] = virtualId; _addValidator(virtualId, founder); - _initValidatorScore(virtualId, founder); return virtualId; } @@ -111,6 +131,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 @@ -118,14 +144,18 @@ 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); } + function initValidatorScore(uint256 virtualId, address validator) public { + _initValidatorScore(virtualId, validator); + } + function _validatorScoreOf( uint256 virtualId, address account @@ -248,4 +278,50 @@ 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; + emit AgentBlacklisted(virtualId, value); + } + + function migrateScoreFunctions() public onlyRole(ADMIN_ROLE) { + _migrateScoreFunctions( + _validatorScoreOf, + totalProposals, + _getPastValidatorScore + ); + } + + function setEloCalculator( + address eloCalculator + ) public onlyRole(ADMIN_ROLE) { + _eloCalculator = eloCalculator; + } + + 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; + } } diff --git a/contracts/virtualPersona/AgentToken.sol b/contracts/virtualPersona/AgentToken.sol index f4a7a48..2185142 100644 --- a/contracts/virtualPersona/AgentToken.sol +++ b/contracts/virtualPersona/AgentToken.sol @@ -1,149 +1,1401 @@ // 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 "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import "../pool/IUniswapV2Router02.sol"; +import "../pool/IUniswapV2Factory.sol"; import "./IAgentToken.sol"; -import "./IAgentNft.sol"; -import "./ERC20Votes.sol"; +import "./IAgentFactory.sol"; -contract AgentToken is IAgentToken, ERC20Upgradeable, ERC20Votes { +contract AgentToken is + ContextUpgradeable, + IAgentToken, + Ownable2StepUpgradeable +{ + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.Bytes32Set; using SafeERC20 for IERC20; - using Checkpoints for Checkpoints.Trace208; - 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 + 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; + + uint128 public maxTokensPerTransaction; + uint128 public maxTokensPerWallet; + address public projectTaxRecipient; + uint128 public projectTaxPendingSwap; + address public vault; // Project supply vault + + 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 + + /** + * @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(); } - modifier canDelegate(address target) { - IAgentNft registry = IAgentNft(virtualNft); - uint256 virtualId = registry.stakingTokenToVirtualId(address(this)); - require( - registry.isValidator(virtualId, target), - "Delegatee is not a validator" + function initialize( + address[3] memory integrationAddresses_, + bytes memory baseParams_, + bytes memory supplyParams_, + bytes memory taxParams_ + ) external initializer { + _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) + ); + + _processSupplyParams(supplyParams); + + uint256 lpSupply = supplyParams.lpSupply * (10 ** decimals()); + uint256 vaultSupply = supplyParams.vaultSupply * (10 ** decimals()); + 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, vaultSupply); + + uniswapV2Pair = _createPair(); + + _factory = IAgentFactory(_msgSender()); + _autoSwapInProgress = true; // We don't want to tax initial liquidity } - mapping(address => Checkpoints.Trace208) private _balanceCheckpoints; + /** + * @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_); - bool internal locked; + (_name, _symbol) = abi.decode(encodedBaseParams_, (string, string)); + } - modifier noReentrant() { - require(!locked, "cannot reenter"); - locked = true; - _; - locked = false; + /** + * @dev function {_processSupplyParams} + * + * Process provided supply params + * + * @param erc20SupplyParameters_ The supply params + */ + 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); } - function initialize( - string memory name, - string memory symbol, - address theFounder, - address theAssetToken, - address theVirtualNft, - uint256 theMatureAt - ) external initializer { - __ERC20_init(name, symbol); - __ERC20Votes_init(); + /** + * @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_, uint256 vaultMint_) internal { + if (lpMint_ > 0) { + _mint(address(this), lpMint_); + } - founder = theFounder; - assetToken = theAssetToken; - virtualNft = theVirtualNft; - matureAt = theMatureAt; + if (vaultMint_ > 0) { + _mint(vault, vaultMint_); + } } - // 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)) - ); + /** + * @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 onlyOwnerOrFactory { + _addInitialLiquidity(lpOwner); } - 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"); + /** + * @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(); } - _burn(sender, amount); - _balanceCheckpoints[sender].push( - clock(), - SafeCast.toUint208(balanceOf(sender)) + // 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} 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 onlyOwnerOrFactory { + // 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} onlyOwnerOrFactory + * + * Allows the manager to remove a liquidity pool + * + * @param removedLiquidityPool_ The address of the old removed liquidity pool + */ + function removeLiquidityPool( + address removedLiquidityPool_ + ) external onlyOwnerOrFactory { + // 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} onlyOwnerOrFactory + * + * Allows the manager to add an unlimited address + * + * @param newUnlimited_ The address of the new unlimited address + */ + function addUnlimited(address newUnlimited_) external onlyOwnerOrFactory { + // Add this to the enumerated list: + _unlimited.add(newUnlimited_); + emit UnlimitedAddressAdded(newUnlimited_); + } + + /** + * @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 onlyOwnerOrFactory { + // 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} 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 onlyOwnerOrFactory { + _validCallerCodeHashes.add(newValidCallerHash_); + emit ValidCallerAdded(newValidCallerHash_); + } + + /** + * @dev function {removeValidCaller} onlyOwnerOrFactory + * + * Allows the owner to remove a valid caller + * + * @param removedValidCallerHash_ The hash of the old removed valid caller + */ + function removeValidCaller( + bytes32 removedValidCallerHash_ + ) external onlyOwnerOrFactory { + // Remove this from the enumerated list: + _validCallerCodeHashes.remove(removedValidCallerHash_); + emit ValidCallerRemoved(removedValidCallerHash_); + } + + /** + * @dev function {setProjectTaxRecipient} onlyOwnerOrFactory + * + * Allows the manager to set the project tax recipient address + * + * @param projectTaxRecipient_ New recipient address + */ + function setProjectTaxRecipient( + address projectTaxRecipient_ + ) external onlyOwnerOrFactory { + projectTaxRecipient = projectTaxRecipient_; + emit ProjectTaxRecipientUpdated(projectTaxRecipient_); + } + + /** + * @dev function {setSwapThresholdBasisPoints} onlyOwnerOrFactory + * + * Allows the manager to set the autoswap threshold + * + * @param swapThresholdBasisPoints_ New swap threshold in basis points + */ + function setSwapThresholdBasisPoints( + uint16 swapThresholdBasisPoints_ + ) external onlyOwnerOrFactory { + uint256 oldswapThresholdBasisPoints = swapThresholdBasisPoints; + swapThresholdBasisPoints = swapThresholdBasisPoints_; + emit AutoSwapThresholdUpdated( + oldswapThresholdBasisPoints, + swapThresholdBasisPoints_ ); + } - IERC20(assetToken).safeTransfer(sender, amount); + /** + * @dev function {setProjectTaxRates} onlyOwnerOrFactory + * + * 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 onlyOwnerOrFactory { + uint16 oldBuyTaxBasisPoints = projectBuyTaxBasisPoints; + uint16 oldSellTaxBasisPoints = projectSellTaxBasisPoints; + + projectBuyTaxBasisPoints = newProjectBuyTaxBasisPoints_; + projectSellTaxBasisPoints = newProjectSellTaxBasisPoints_; + + emit ProjectTaxBasisPointsChanged( + oldBuyTaxBasisPoints, + newProjectBuyTaxBasisPoints_, + oldSellTaxBasisPoints, + newProjectSellTaxBasisPoints_ + ); } - function getPastBalanceOf( - address account, - uint256 timepoint - ) public view returns (uint256) { - uint48 currentTimepoint = clock(); - if (timepoint >= currentTimepoint) { - revert ERC5805FutureLookup(timepoint, currentTimepoint); + /** + * @dev function {setLimits} onlyOwnerOrFactory + * + * 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 onlyOwnerOrFactory { + uint256 oldMaxTokensPerTransaction = maxTokensPerTransaction; + uint256 oldMaxTokensPerWallet = maxTokensPerWallet; + // Limit can only be increased: + if ( + (oldMaxTokensPerTransaction == 0 && + newMaxTokensPerTransaction_ != 0) || + (oldMaxTokensPerWallet == 0 && newMaxTokensPerWallet_ != 0) + ) { + revert LimitsCanOnlyBeRaised(); } - return - _balanceCheckpoints[account].upperLookupRecent( - SafeCast.toUint48(timepoint) - ); + 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; } - // This is non-transferable token + /** + * @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 /*value*/ - ) public override returns (bool) { - revert("Transfer not supported"); + address to, + uint256 amount + ) public virtual override(IERC20) returns (bool) { + address owner = _msgSender(); + _transfer( + owner, + to, + amount, + (isLiquidityPool(owner) || isLiquidityPool(to)) + ); + return true; } - function transferFrom( - address /*from*/, - address /*to*/, - uint256 /*value*/ - ) public override returns (bool) { - revert("Transfer not supported"); + /** + * @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 /*value*/ - ) public override returns (bool) { - revert("Approve not supported"); + 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; } - // The following functions are overrides required by Solidity. - function _update( + /** + * @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 value - ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._update(from, to, value); + 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 { + address[] memory path = new address[](2); + path[0] = address(this); + path[1] = pairToken; + + // Wrap external calls in try / catch to handle errors + try + _uniswapRouter + .swapExactTokensForTokensSupportingFeeOnTransferTokens( + swapBalance_, + 0, + path, + projectTaxRecipient, + block.timestamp + 600 + ) + { + // 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; + } + } 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} onlyOwnerOrFactory + * + * 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 onlyOwnerOrFactory { + (bool success, ) = _msgSender().call{value: amount_}(""); + if (!success) { + revert TransferFailed(); + } + } + + /** + * @dev function {withdrawERC20} onlyOwnerOrFactory + * + * 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 onlyOwnerOrFactory { + 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); } - function getPastDelegates( - address account, - uint256 timepoint - ) public view returns (address) { - return super._getPastDelegates(account, timepoint); + /** + * @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 {} } diff --git a/contracts/virtualPersona/AgentVeToken.sol b/contracts/virtualPersona/AgentVeToken.sol new file mode 100644 index 0000000..436edee --- /dev/null +++ b/contracts/virtualPersona/AgentVeToken.sol @@ -0,0 +1,181 @@ +// 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"; +import "@openzeppelin/contracts/access/IAccessControl.sol"; + +contract AgentVeToken is IAgentVeToken, ERC20Upgradeable, ERC20Votes { + using SafeERC20 for IERC20; + using Checkpoints for Checkpoints.Trace208; + + 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 + uint256 public initialLock; // Initial locked amount + + constructor() { + _disableInitializers(); + } + + 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, + address _assetToken, + uint256 _matureAt, + address _agentNft, + bool _canStake + ) external initializer { + __ERC20_init(_name, _symbol); + __ERC20Votes_init(); + + founder = _founder; + matureAt = _matureAt; + assetToken = _assetToken; + agentNft = _agentNft; + canStake = _canStake; + } + + // Stakers have to stake their tokens and delegate to a validator + 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"); + require( + IERC20(assetToken).balanceOf(sender) >= amount, + "Insufficient asset token balance" + ); + require( + IERC20(assetToken).allowance(sender, address(this)) >= amount, + "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; + } + + registry.addValidator(virtualId, delegatee); + + IERC20(assetToken).safeTransferFrom(sender, address(this), amount); + _mint(receiver, amount); + _delegate(receiver, delegatee); + _balanceCheckpoints[receiver].push( + clock(), + SafeCast.toUint208(balanceOf(receiver)) + ); + } + + function setCanStake(bool _canStake) public { + require(_msgSender() == founder, "Not founder"); + 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"); + + if ( + (sender == founder) && ((balanceOf(sender) - amount) < initialLock) + ) { + 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); + } +} 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 829c5e9..0e0bc1f 100644 --- a/contracts/virtualPersona/IAgentDAO.sol +++ b/contracts/virtualPersona/IAgentDAO.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "@openzeppelin/contracts/governance/utils/IVotes.sol"; import "@openzeppelin/contracts/governance/IGovernor.sol"; +import "@openzeppelin/contracts/governance/utils/IVotes.sol"; interface IAgentDAO { function initialize( string memory name, IVotes token, - address contributionNft, + address agentNft, uint256 threshold, uint32 votingPeriod_ ) external; @@ -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/IAgentFactory.sol b/contracts/virtualPersona/IAgentFactory.sol new file mode 100644 index 0000000..0578a79 --- /dev/null +++ b/contracts/virtualPersona/IAgentFactory.sol @@ -0,0 +1,21 @@ +// 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); +} diff --git a/contracts/virtualPersona/IAgentNft.sol b/contracts/virtualPersona/IAgentNft.sol index 95babfe..c88a9a8 100644 --- a/contracts/virtualPersona/IAgentNft.sol +++ b/contracts/virtualPersona/IAgentNft.sol @@ -5,7 +5,7 @@ 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 @@ -14,12 +14,20 @@ interface IAgentNft is IValidatorRegistry { 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, string memory newTokenURI, address payable theDAO, address founder, - uint8[] memory coreTypes + uint8[] memory coreTypes, + address pool, + address token ) external returns (uint256); function stakingTokenToVirtualId( @@ -32,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); @@ -50,4 +62,10 @@ interface IAgentNft is IValidatorRegistry { function getAllServices( uint256 virtualId ) external view returns (uint256[] memory); + + 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 1eedcae..5f58a5c 100644 --- a/contracts/virtualPersona/IAgentToken.sol +++ b/contracts/virtualPersona/IAgentToken.sol @@ -1,31 +1,351 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -interface IAgentToken { - function initialize( - string memory name, - string memory symbol, - address theFounder, - address theAssetToken, - address theVirtualNft, - uint256 theMatureAt +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; - function stake( - uint256 amount, - address receiver, - address delegatee + /** + * @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; - function withdraw(uint256 amount) 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; - function getPastDelegates( - address account, - uint256 timepoint - ) external view returns (address); + /** + * @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; - function getPastBalanceOf( - address account, - uint256 timepoint - ) external view returns (uint256); + /** + * @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( + address[3] memory integrationAddresses_, + bytes memory baseParams_, + bytes memory supplyParams_, + bytes memory taxParams_ + ) external; } diff --git a/contracts/virtualPersona/IAgentVeToken.sol b/contracts/virtualPersona/IAgentVeToken.sol new file mode 100644 index 0000000..c03fd44 --- /dev/null +++ b/contracts/virtualPersona/IAgentVeToken.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IAgentVeToken { + function initialize( + string memory _name, + string memory _symbol, + address _founder, + address _assetToken, + uint256 _matureAt, + address _agentNft, + bool _canStake + ) 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); +} 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..6d88a12 --- /dev/null +++ b/contracts/virtualPersona/IERC20Config.sol @@ -0,0 +1,33 @@ +// 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 maxSupply; + uint256 lpSupply; + uint256 vaultSupply; + uint256 maxTokensPerWallet; + uint256 maxTokensPerTxn; + uint256 botProtectionDurationInSeconds; + address vault; + } + + struct ERC20TaxParameters { + uint256 projectBuyTaxBasisPoints; + uint256 projectSellTaxBasisPoints; + uint256 taxSwapThresholdBasisPoints; + address projectTaxRecipient; + } +} \ No newline at end of file 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/contracts/virtualPersona/IErrors.sol b/contracts/virtualPersona/IErrors.sol new file mode 100644 index 0000000..a8bb725 --- /dev/null +++ b/contracts/virtualPersona/IErrors.sol @@ -0,0 +1,313 @@ +// 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 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. + + 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/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/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/hardhat.config.js b/hardhat.config.js index 2ccc4f4..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 = { @@ -72,6 +73,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], @@ -84,8 +88,5 @@ module.exports = { url: "https://rpc.ankr.com/eth_goerli", accounts: [process.env.PRIVATE_KEY], }, - }, - sourcify: { - enabled: true, - }, + } }; 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/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/minter.js b/scripts/arguments/minter.js new file mode 100644 index 0000000..5bea7ee --- /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.IMPACT_MULTIPLIER, + 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/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/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/upgradeRewards.ts b/scripts/upgradeRewards.ts deleted file mode 100644 index 46eb3f8..0000000 --- a/scripts/upgradeRewards.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ethers, upgrades } from "hardhat"; - -(async () => { - try { - const Contract = await ethers.getContractFactory("AgentRewardV2"); - const contract = await upgrades.upgradeProxy(process.env.PERSONA_REWARD, Contract); - console.log("Upgraded", contract.target) - } catch (e) { - console.log(e); - } -})(); 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 76% rename from scripts/deployTimeLock.ts rename to scripts/v1/deployTimeLock.ts index d80d418..c48854d 100644 --- a/scripts/deployTimeLock.ts +++ b/scripts/v1/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) 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/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/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); + } +})(); diff --git a/scripts/v2/migrateV2.ts b/scripts/v2/migrateV2.ts new file mode 100644 index 0000000..0980594 --- /dev/null +++ b/scripts/v2/migrateV2.ts @@ -0,0 +1,120 @@ +import { ethers, upgrades } from "hardhat"; + +const adminSigner = new ethers.Wallet( + process.env.ADMIN_PRIVATE_KEY, + ethers.provider +); + +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 + ); + 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_LP_SUPPLY, + process.env.AGENT_TOKEN_VAULT_SUPPLY, + process.env.AGENT_TOKEN_LIMIT, + process.env.AGENT_TOKEN_LIMIT, + process.env.BOT_PROTECTION, + process.env.MINTER + ); + await factory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + process.env.TAX_VAULT + ); + + console.log("Upgraded FactoryV2", factory.target); +} + +async function importContract() { + const ContractFactory = await ethers.getContractFactory( + "AgentNftV2", + adminSigner + ); + await upgrades.forceImport( + "0x8f767259867Cc93df37e59ba445019418871ea57", + 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 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"); + + const contract = await ethers.getContractAt( + "AgentNftV2", + process.env.VIRTUAL_NFT + ); + await contract.setEloCalculator(process.env.ELO); +} + +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: null, veTokenImpl: null }; +} + +(async () => { + try { + //const implementations = await deployImplementations(); + await upgradeFactory(); + //await deployAgentNft(); + //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 new file mode 100644 index 0000000..863cb41 --- /dev/null +++ b/test/agentDAO.js @@ -0,0 +1,329 @@ +const { expect } = require("chai"); +const { toBeHex } = require("ethers/utils"); +const { + loadFixture, + mine, +} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); +const { parseEther } = require("ethers"); + +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 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("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 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.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, + minter.target + ); + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.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, false); + + 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, + }, + }; + } + + before(async function () {}); + + it("should auto early execution when forVotes == totalSupply", async function () { + const base = await loadFixture(deployWithAgent); + const { founder } = await getAccounts(); + const { agent, serviceNft } = 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]; + + // Proposal not executed yet + await expect(serviceNft.ownerOf(proposalId)).to.be.reverted; + await mine(10); + await agentDAO + .connect(founder) + .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(7n); + await expect(agentDAO.execute(proposalId)).to.be.reverted; + }); + + 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/contribution.js b/test/contribution.js index a493a02..18a4120 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,9 +7,17 @@ const { mine, } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); -const getExecuteCallData = (factory, proposalId) => { - return factory.interface.encodeFunctionData("executeApplication", [ - proposalId, +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, ]); }; @@ -27,211 +26,300 @@ 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 demoToken = await ethers.deployContract( - "BMWToken", - [deployer.address], + 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"), + [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], - {} + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + 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, + ] ); - - const service = await upgrades.deployProxy( - await ethers.getContractFactory("ServiceNft"), - [personaNft.target, contribution.target, process.env.DATASET_SHARES], + await minter.waitForDeployment(); + 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, + minter.target + ); + 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 personaNft.setContributionService( - contribution.target, - service.target - ); + 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 { virtualId, token, dao, tba } = factoryEvent.args; - const persona = { virtualId, token, dao, tba }; - return { ...contracts, persona }; + 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 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, + parentId, + isModel, + datasetId, + desc, + base, + account, + 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 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(["uint8[] memory"], [votes]) : "0x"; + + for (const voter of voters) { + await agentDAO + .connect(voter) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + } + + await mine(600); + + if (isModel) { + await minter.mint(proposalId); + } + + return proposalId; } function getDescHash(str) { @@ -244,407 +332,435 @@ describe("Contribution", function () { this.signers = signers; }); - it("should be able to mint a new contribution", async function () { - const { proposalId, contribution, persona } = await loadFixture( - proposeContribution + it("should be able to mint a new contribution NFT", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1, founder } = await getAccounts(); + 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); + + const contributionId = await createContribution( + 1, + 0, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - // Mint contribution - await expect( - contribution.mint( - this.accounts[1], - persona.virtualId, - 0, - TOKEN_URI, - proposalId, - "0x0000000000000000000000000000000000000000", - true, - 0 - ) - ).to.emit(contribution, "NewContribution"); - - 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 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, 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]] - ); + 0, + "Test", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + const balance2 = await agentToken.balanceOf(contributor1.address); + expect(balance2).to.equal(parseEther("100000")); + }); - await personaDAO.castVoteWithReasonAndParams( - proposalId, + 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 agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token + ); + const balance1 = await agentToken.balanceOf(ipVault.address); + expect(balance1).to.equal(0n); + await createContribution( 1, - "lfg", - voteParams + 0, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - 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(ipVault.address); + expect(balance2).to.equal(parseEther("10000")); }); - it("should set correct maturity 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 { contributor1, contributor2, founder } = await getAccounts(); + const agentToken = await ethers.getContractAt( + "AgentToken", + base.agent.token ); - await contribution.mint( - this.accounts[1], - persona.virtualId, + + // 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] + ); + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); + + await createContribution( + 1, + 0, 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]] + c1, + "Test model", + base, + contributor2.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - 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(contributor2.address); + expect(balance2).to.equal(parseEther("30000")); + + const balance12 = await agentToken.balanceOf(contributor1.address); + expect(balance12).to.equal(parseEther("70000")); }); - it("should show correct impact score", async function () { - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + it("should allow adjusting global agent token multiplier", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1, contributor2, founder } = await getAccounts(); + const { minter } = base; + 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, + await minter.setImpactMultiplier(20000000); //2x + + // No agent token minted for dataset contribution + const c1 = await createContribution( 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 + 0, + 0, + false, + 0, + "Dataset", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - 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 balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); + + await createContribution( + 1, + 0, 0, - TOKEN_URI, - proposalId2, - proposalId, true, - 0 + c1, + "Test model", + base, + contributor2.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - 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); + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("60000")); + + const balance12 = await agentToken.balanceOf(contributor1.address); + expect(balance12).to.equal(parseEther("140000")); }); - it("should allow contribution admin to create proposal", async () => { - 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 { contributor1, contributor2, founder } = await getAccounts(); + const { minter, agent } = base; + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + await minter.setImpactMulOverride(agent.virtualId, 20000000); //2x + // 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] ); - 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"); + const balance1 = await agentToken.balanceOf(contributor1.address); + expect(balance1).to.equal(0n); - await expect( - personaDAO - .connect(signers[14]) - .propose([personaDAO.target], [0], [ethers.ZeroAddress], "Test2") - ).to.be.reverted; + await createContribution( + 1, + 0, + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); - await contribution.connect(signers[15]).setAdmin(signers[14].address); + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("60000")); - 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("140000")); }); - 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); + it("should not allow adjusting agent token multiplier by public", async function () { + const base = await loadFixture(deployWithAgent); + const { contributor1, contributor2, founder } = await getAccounts(); + const { minter, agent } = base; + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + + await expect( + minter.connect(founder).setImpactMulOverride(agent.virtualId, 20000) + ).to.be.reverted; - 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, + 0, + false, + 0, + "Dataset", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); + 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, + 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); + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + + const balance2 = await agentToken.balanceOf(contributor2.address); + expect(balance2).to.equal(parseEther("30000")); + + const balance12 = await agentToken.balanceOf(contributor1.address); + expect(balance12).to.equal(parseEther("70000")); }); - 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); + it("should be able to adjust the global IP share", async function () { + const base = await loadFixture(deployWithAgent); + const { deployer, contributor1, ipVault, founder } = await getAccounts(); + const { minter, agent } = base; + 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 minter.connect(deployer).setIPShare(5000); - await personaDAO.castVoteWithReasonAndParams( - proposalId, + await createContribution( + 1, + 0, + 0, + true, 0, - "lfg", - voteParams + "Test model", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - await expect( - personaDAO.castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams) - ).to.be.reverted; + + const balance = await agentToken.balanceOf(ipVault.address); + expect(balance).to.equal(parseEther("50000")); }); - it("should not increase validator score with deliberate votes", async function () { - const signers = await ethers.getSigners(); - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + it("should be able to adjust the IP share per agent", async function () { + 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 ); - 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]; - expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 3, - "lfg", - voteParams + 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] ); - 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 + await createContribution( + 1, + 0, + 0, + true, + c1, + "Test model", + base, + contributor2.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - 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 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 ); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(0); - await personaDAO.castVoteWithReasonAndParams( - proposalId, + // First contribution always gets 100 impact + await createContribution( 1, - "lfg", - voteParams - ); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(1n); - await personaDAO.castVoteWithReasonAndParams( - proposalId, - 3, - "lfg", - voteParams + 0, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - expect(await personaDAO.scoreOf(validator1)).to.be.equal(1n); + + 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 not emit ValidatorEloRating for deliberate votes", async function () { - const signers = await ethers.getSigners(); - const { persona, proposalId, service, contribution } = await loadFixture( - proposeContribution + 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 ); - const personaDAO = await ethers.getContractAt("AgentDAO", persona.dao); - // Only contribution proposal will emit ValidatorEloRating event - await contribution.mint( - this.accounts[1], - persona.virtualId, + // First contribution always gets 100 impact + await createContribution( + 1, + 0, 0, - TOKEN_URI, - proposalId, - "0x0000000000000000000000000000000000000000", true, - 0 + 0, + "Test", + base, + contributor1.address, + [founder], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ); - const voteParams = abi.encode( - ["uint256", "uint8[] memory"], - [20, [0, 1, 1, 0, 2]] + 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] ); - 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"); + 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 325b989..cbf80bb 100644 --- a/test/delegate.js +++ b/test/delegate.js @@ -1,24 +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, - ]); -}; - 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", @@ -33,131 +24,153 @@ 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 - ); - - 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, - 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 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.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, + minter.target ); - - 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 ); - const filter = personaFactory.filters.NewApplication; - const events = await personaFactory.queryFilter(filter, -1); + return { virtualToken, agentFactory, agentNft }; + } + + 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 () { @@ -167,49 +180,42 @@ 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) - ).to.equal(account2); - expect( - await personaTokenContract.getPastDelegates(account1, block3) - ).to.equal(account3); - expect( - await personaTokenContract.getPastDelegates(account1, block1) - ).to.equal(account1); - expect(await personaTokenContract.delegates(account1)).to.equal(account3); + 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); }); 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( - account1, - blockNumber + i + 1 - ) + await veToken.getPastDelegates(account1, blockNumber + i + 1) ).to.equal(this.accounts[i]); } }); diff --git a/test/deprecated/contribution.js.old b/test/deprecated/contribution.js.old new file mode 100644 index 0000000..e709025 --- /dev/null +++ b/test/deprecated/contribution.js.old @@ -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/genesisDAO.js b/test/deprecated/genesisDAO.js.old similarity index 100% rename from test/genesisDAO.js rename to test/deprecated/genesisDAO.js.old diff --git a/test/rewards.js b/test/deprecated/rewards.js.old similarity index 100% rename from test/rewards.js rename to test/deprecated/rewards.js.old diff --git a/test/staking.js b/test/deprecated/staking.js.old similarity index 100% rename from test/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 48f0960..cbf4578 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]); }; @@ -29,16 +16,10 @@ 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 TREASURY_AMOUNT = parseEther("1000000"); //1M + const MATURITY_SCORE = toBeHex(2000, 32); // 20% + const IP_SHARE = 1000; // 10% const TOKEN_URI = "http://jessica"; @@ -50,257 +31,267 @@ 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, + 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, ipVault, treasury, virtualTreasury } = + await getAccounts(); - const [deployer] = await ethers.getSigners(); - const veToken = await ethers.deployContract( - "veVirtualToken", - [deployer.address], + const virtualToken = await ethers.deployContract( + "VirtualToken", + [TREASURY_AMOUNT, deployer.address], {} ); - await veToken.waitForDeployment(); + await virtualToken.waitForDeployment(); - const demoToken = await ethers.deployContract( - "BMWToken", - [deployer.address], + 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"), + [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 + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + 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, + ] ); - - const reward = await ethers.deployContract("AgentRewardX2", [], {}); - await reward.waitForDeployment(); - - const contributionNft = await upgrades.deployProxy( - await ethers.getContractFactory("ContributionNft"), - [personaNft.target], - {} + await minter.waitForDeployment(); + 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, + minter.target ); - - const serviceNft = await upgrades.deployProxy( - await ethers.getContractFactory("ServiceNft"), - [personaNft.target, contributionNft.target, process.env.DATASET_SHARES], - {} + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address ); - - await personaNft.setContributionService( - contributionNft.target, - serviceNft.target + await agentFactory.grantRole( + await agentFactory.WITHDRAW_ROLE(), + deployer.address ); - await reward.initialize( - demoToken.target, - personaNft.target, - contributionNft.target, - serviceNft.target, - { - protocolShares: 1000, - contributorShares: 5000, - stakerShares: 9000, - parentShares: 2000, - stakeThreshold: "1000000000000000000000", - } + const rewards = await upgrades.deployProxy( + await ethers.getContractFactory("AgentRewardV2"), + [ + virtualToken.target, + agentNft.target, + { + protocolShares: process.env.PROTOCOL_SHARES, + stakerShares: process.env.STAKER_SHARES, + }, + ], + {} ); - const role = await reward.GOV_ROLE(); - await reward.grantRole(role, signers[0].address); - + await rewards.waitForDeployment(); + await rewards.grantRole(await rewards.GOV_ROLE(), deployer.address); + await mine(1); return { - reward, - veToken, - protocolDAO, - demoToken, - personaFactory, - personaNft, - contributionNft, - serviceNft, + virtualToken, + agentFactory, + agentNft, + serviceNft: service, + contributionNft: contribution, + minter, + rewards, }; } - async function deployGenesisVirtual() { - 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); + async function deployWithApplication() { + const base = await deployBaseContracts(); - 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]; + 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]; - - const { virtualId, token, dao } = factoryEvent.args; - const persona = { virtualId, token, dao }; - return { ...contracts, persona }; + return factoryEvent.args; } - 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); + async function deployWithAgent() { + const base = await deployWithApplication(); + const { applicationId } = base; - // 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]); + const { founder } = await getAccounts(); - return { ...base }; + 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 + account, + voters, + votes ) { - const signers = await ethers.getSigners(); - const { persona, serviceNft, contributionNft } = base; - const personaDAO = await ethers.getContractAt("AgentDAO", persona.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, - persona.virtualId, + 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, + virtualId, coreId, TOKEN_URI, proposalId, @@ -308,401 +299,1060 @@ describe("RewardsV2", function () { isModel, datasetId ); + const voteParams = isModel ? abi.encode(["uint8[] memory"], [votes]) : "0x"; + + for (const voter of voters) { + await agentDAO + .connect(voter) + .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); + } - 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]) - .castVoteWithReasonAndParams(proposalId, 1, "lfg", voteParams); await mine(600); - await personaDAO.execute(proposalId); + await minter.mint(proposalId); 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 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(); + // 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, 0, - false, + true, 0, - "LLM DS", + "Test", base, - account + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) ); - contributionList.push(nft); + await expect( + rewards.distributeRewards(parseEther(rewardSize.toString()), [1], false) + ).to.not.be.reverted; + }); - // NFT 2 (LLM Model) - 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(); + // 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, - 200, 0, true, - nft, - "LLM Model", + 0, + "Test", base, - account + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + 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"); + + // 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]) ); - contributionList.push(nft); + expect(validatorSClaimable).to.equal("0.0"); + expect(validatorVClaimable).to.equal("10000.0"); - // NFT 3 (Voice DS) - nft = await createContribution( + // 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(); + // 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, 0, - false, + true, 0, - "Voice DS", + "Test", base, - account + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) ); - contributionList.push(nft); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + true + ); + 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"); + + // 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"); + }); + + it("should be able to distribute protocol emission for multiple virtuals with arbitrary LP values", async function () { + const base = await loadFixture(deployWithAgent); + 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); + 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 + ); + + await veToken.connect(founder).delegate(validator1.address); + await createContribution( + i, + 0, + 0, + true, + 0, + `Test ${i}`, + base, + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + } + + 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 { rewards, virtualToken, agent, agentNft } = base; + const { contributor1, founder, validator1, trader, validator2 } = + await getAccounts(); + // 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); - // NFT 4 (Voice Model *current) - nft = await createContribution( + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + + 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, - 100, + 0, 0, true, - nft, - "Voice Model", + 0, + "Test", base, - account + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + false ); - contributionList.push(nft); + 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 be able to distribute rewards based on validator uptime (validator2 is up)", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent, agentNft } = base; + const { contributor1, founder, validator1, trader, validator2 } = + await getAccounts(); + + 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 + ); + + // Trader buys tokens + await virtualToken.mint(trader.address, parseEther("300000")); + await virtualToken + .connect(trader) + .approve(router.target, parseEther("300000")); + const agentTokenAddr = (await agentNft.virtualInfo(1)).token; + const amountToBuy = parseEther("10000000"); + const capital = parseEther("300000"); + 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)); + + // Add liquidity + 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); + // 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 + ); + await mine(1); - nft = await createContribution( - 2, - 100, + // Validator 1 voting + await createContribution( + 1, + 0, 0, true, 0, - "Visual Model", + "Test", base, - account + contributor1.address, + [validator1, validator2], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + false ); - contributionList.push(nft); + await mine(1); - 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]); + const v1 = parseFloat( + formatEther(await veToken.balanceOf(founder.address)) + ); + const v2 = parseFloat(formatEther(await veToken.balanceOf(trader.address))); - return { contributionList, ...base }; - } + // Staker1 + Validator 1 + const staker1SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(founder.address, [1]) + ); + const staker1VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(founder.address, [1]) + ); - before(async function () { - const signers = await ethers.getSigners(); - this.accounts = signers.map((signer) => signer.address); - this.signers = signers; - }); + expect(parseFloat(staker1SClaimable).toFixed(4)).to.be.equal("72964.6543"); + expect(staker1VClaimable).to.equal("0.0"); - 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, - ]); + 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).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).toFixed(4)).to.be.equal("17035.3457"); + 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).toFixed(4)).to.be.equal( + "1892.8162" + ); }); - it("should calculate correct staker and validator rewards", async function () { - const { reward, persona } = await loadFixture(stakeAndVote); - const [validator1, staker1, validator2, staker2] = this.accounts; + 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); - 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"); + await createContribution( + 1, + 0, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await expect( + rewards.distributeRewards(parseEther(rewardSize.toString()), [1], false) + ).to.not.be.reverted; }); - it("should withdraw correct staker and validator rewards", async function () { - const { reward, demoToken } = await loadFixture(stakeAndVote); - const [validator1, staker1, validator2, staker2] = this.signers; + 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(); + + // 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); - expect(await demoToken.balanceOf(reward.target)).to.be.equal( - parseEther("2000") + await createContribution( + 1, + 0, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + 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"); - expect(await demoToken.balanceOf(validator1.address)).to.be.equal( - parseEther("0") + // Validator has 10% of the total rewards + const validatorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(validator1.address, [1]) ); - expect(await demoToken.balanceOf(staker1.address)).to.be.equal( - parseEther("0") + const validatorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(validator1.address, [1]) ); - expect(await demoToken.balanceOf(validator2.address)).to.be.equal( - parseEther("0") + expect(validatorSClaimable).to.equal("0.0"); + expect(validatorVClaimable).to.equal("10000.0"); + + // Nothing for contributor + const conributorSClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(contributor1.address, [1]) ); - expect(await demoToken.balanceOf(staker2.address)).to.be.equal( - parseEther("0") + const contributorVClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(contributor1.address, [1]) ); + expect(conributorSClaimable).to.equal("0.0"); + expect(contributorVClaimable).to.equal("0.0"); + }); - await reward.claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(validator1))).toFixed(4) - ).to.be.equal("60.2679"); + 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(); - await reward.connect(staker1).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(staker1))).toFixed(4) - ).to.be.equal("361.6071"); + // 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 reward.connect(validator2).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(validator2))).toFixed(4) - ).to.be.equal("41.7857"); + await createContribution( + 1, + 0, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + const rewardSize = 100000; + await virtualToken.approve( + rewards.target, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1], + true + ); + 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 reward.connect(staker2).claimAllRewards([1], []); - expect( - parseFloat(formatEther(await demoToken.balanceOf(staker2))).toFixed(4) - ).to.be.equal("14.4643"); + // 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"); - // 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"); + // 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 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); + it("should be able to distribute protocol emission for multiple virtuals with arbitrary LP values", async function () { + const base = await loadFixture(deployWithAgent); + 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 lp3 = await rewards.getLPValue(agent3[0]); + console.log("Initial LP ", lp3); + + // Create contributions for all 4 virtuals + for (let i = 1; i <= 4; i++) { + let veToken = await ethers.getContractAt( + "AgentVeToken", + ( + await agentNft.virtualLP(i) + ).veToken + ); - 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); + await veToken.connect(founder).delegate(validator1.address); + await createContribution( + i, + 0, + 0, + true, + 0, + `Test ${i}`, + base, + trader.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + } - 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"); - }); + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); + // Add liquidity of 0.25, 0.75 and 1 + await virtualToken.mint(trader.address, parseEther("400")); + await virtualToken + .connect(trader) + .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 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( + virtualToken.target, + agentTokenAddr, + amountToAdd, + amountToAdd, + 0, + 0, + trader.address, + 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); + await mine(1); + } + + // Distribute rewards + // Expectations: + // 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(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, + parseEther(rewardSize.toString()) + ); + await rewards.distributeRewards( + parseEther(rewardSize.toString()), + [1, 3, 4], + false + ); + await mine(1); + const rewards1 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [1] + ); - it("should claim correct model contributor rewards", async function () { - const { contributionList, reward, serviceNft, demoToken } = - await loadFixture(prepareContributions); + //99999583336111092592716 + const rewardSizeAfterProtocol = + (rewardSizeInGwei * BigInt(process.env.STAKER_SHARES)) / BigInt(10000); + const expectedRewardLP1Ratio = + (await (rewardSizeAfterProtocol * Lp1)) / totalLp; - const taxCollector = this.signers[10]; - expect(await demoToken.balanceOf(taxCollector.address)).to.be.equal(0); + const rewards2 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [2] + ); + const rewards3 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [3] + ); + const expectedRewardLP3Ratio = + (await (rewardSizeAfterProtocol * Lp3)) / totalLp; + + const rewards4 = await rewards.getTotalClaimableStakerRewards( + founder.address, + [4] + ); + const expectedRewardLP4Ratio = (rewardSizeAfterProtocol * Lp4) / totalLp; - 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; + await parseInt(ethers.formatUnits(rewards1.toString(), "gwei")) + ).to.be.equal(expectedRewardLP1Ratio); expect( - formatEther(await demoToken.balanceOf(taxCollector.address)) - ).to.be.equal("600.0"); - await expect( - reward.connect(taxCollector).claimAllRewards([], [contributionList[4]]) - ).to.be.fulfilled; + await parseInt(ethers.formatUnits(rewards3.toString(), "gwei")) + ).to.be.equal(expectedRewardLP3Ratio); expect( - formatEther(await demoToken.balanceOf(taxCollector.address)) - ).to.be.equal("900.0"); + await parseInt(ethers.formatUnits(rewards4.toString(), "gwei")) + ).to.be.equal(expectedRewardLP4Ratio); - // 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"); + expect(rewards4).to.be.greaterThan(rewards3); + expect(rewards3).to.be.greaterThan(rewards1); + expect(rewards2).to.be.equal(0n); }); - 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]; + it("should be able to distribute rewards based on validator uptime (validator2 is down)", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent, agentNft } = base; + const { contributor1, founder, validator1, trader, validator2 } = + await getAccounts(); + // 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 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"); + const router = await ethers.getContractAt( + "IUniswapV2Router02", + process.env.UNISWAP_ROUTER + ); - await expect( - reward.connect(taxCollector).claimAllRewards([], contributionList) - ).to.be.fulfilled; - expect( - parseFloat(formatEther(await demoToken.balanceOf(taxCollector))).toFixed( - 4 - ) - ).to.be.equal("900.0000"); - }); + 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, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [validator1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + 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"); - it("should withdraw correct protocol rewards", async function () { - const { contributionList, reward, serviceNft, demoToken } = - await loadFixture(prepareContributions); - const treasury = this.signers[9]; + // 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"); - expect(await demoToken.balanceOf(treasury.address)).to.be.equal( - parseEther("0") + const validator2SClaimable = formatEther( + await rewards.getTotalClaimableStakerRewards(trader.address, [1]) ); - await reward.withdrawProtocolRewards(treasury.address); - expect(await demoToken.balanceOf(treasury.address)).to.be.equal( - parseEther("400") + const validator2VClaimable = formatEther( + await rewards.getTotalClaimableValidatorRewards(trader.address, [1]) ); + expect(validator2SClaimable).to.equal("0.0"); + expect(validator2VClaimable).to.equal("0.0"); }); - it("should withdraw correct validator pool rewards", async function () { - const { contributionList, reward, serviceNft, demoToken } = - await loadFixture(prepareContributions); - const treasury = this.signers[9]; + it("should be able to distribute rewards based on validator uptime (validator2 is up)", async function () { + const base = await loadFixture(deployWithAgent); + const { rewards, virtualToken, agent, agentNft } = base; + const { contributor1, founder, validator1, trader, validator2 } = + await getAccounts(); - expect(await demoToken.balanceOf(treasury.address)).to.be.equal( - parseEther("0") + 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 ); - await reward.withdrawValidatorPoolRewards(treasury.address); - expect( - parseFloat( - formatEther(await demoToken.balanceOf(treasury.address)) - ).toFixed(4) - ).to.be.equal("542.4107"); + + 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("10000000"); // 20% of the pool + const capital = parseEther("200000"); + 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, + 0, + true, + 0, + "Test", + base, + contributor1.address, + [validator1, validator2], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ); + 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(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 859af73..b05b29e 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("50000"); // 50k const MATURITY_SCORE = toBeHex(2000, 32); // 20% + const IP_SHARE = 1000; // 10% const genesisInput = { name: "Jessica", @@ -34,154 +33,182 @@ 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, 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, 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 + await agentFactory.waitForDeployment(); + await agentNft.grantRole(await agentNft.MINTER_ROLE(), agentFactory.target); + 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.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, + minter.target + ); + + await agentFactory.setTokenTaxParams( + process.env.TAX, + process.env.TAX, + process.env.SWAP_THRESHOLD, + treasury.address ); - return { veToken, protocolDAO, demoToken, personaFactory, personaNft }; + return { virtualToken, agentFactory, agentNft, 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 - ); - - 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, + 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 }; - 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 +217,555 @@ describe("AgentFactory", function () { genesisInput.tbaImplementation, genesisInput.daoVotingPeriod, genesisInput.daoThreshold - ) - ).to.be.revertedWith("Insufficient asset token"); - }); + ); + expect(tx).to.emit(agentFactory, "NewApplication"); - it("should propose a new Persona", async function () { - const { personaFactory, personaNft, demoToken } = await loadFixture( - deployBaseContracts - ); + expect(await virtualToken.balanceOf(founder.address)).to.be.equal(0n); - // 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(await demoToken.balanceOf(this.accounts[0])).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, false) + ).to.emit(agentFactory, "NewPersona"); + + // Check genesis components + // C1: Agent Token + // C2: LP Pool + Initial liquidity + // C3: Agent veToken + // C4: Agent DAO + // C5: Agent NFT + // C6: TBA + // C7: 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, minter } = await loadFixture(deployWithAgent); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + expect(await agentToken.totalSupply()).to.be.equal( + parseEther(process.env.AGENT_TOKEN_LIMIT) ); - - 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(PROTOCOL_DAO_VOTING_PERIOD); - - await expect(protocolDAO.execute(daoProposalId)).to.emit( - personaFactory, - "NewPersona" + expect(await agentToken.balanceOf(minter.target)).to.be.equal( + parseEther(process.env.AGENT_TOKEN_VAULT_SUPPLY) ); - const factoryFilter = personaFactory.filters.NewPersona; - const factoryEvents = await personaFactory.queryFilter(factoryFilter, -1); - const factoryEvent = factoryEvents[0]; + }); + + it("agent component C2: LP Pool", async function () { + const { agent, virtualToken } = await loadFixture(deployWithAgent); + const lp = await ethers.getContractAt("IUniswapV2Pair", agent.lp); - const { virtualId, token, dao, tba } = factoryEvent.args; - const persona = { virtualId, token, dao, tba }; + const t0 = await lp.token0(); + const t1 = await lp.token1(); - // 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 addresses = [agent.token, virtualToken.target]; + expect(addresses).contain(t0); + expect(addresses).contain(t1); - 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 + // t0 and t1 will change position dynamically + const reserves = await lp.getReserves(); + expect(reserves[0]).to.be.equal( + t0 === agent.token + ? parseEther(process.env.AGENT_TOKEN_LP_SUPPLY) + : PROPOSAL_THRESHOLD ); - expect(await daoInstance.votingPeriod()).to.equal( - genesisInput.daoVotingPeriod + expect(reserves[1]).to.be.equal( + t1 === agent.token + ? parseEther(process.env.AGENT_TOKEN_LP_SUPPLY) + : PROPOSAL_THRESHOLD ); + }); - 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 C3: Agent veToken", async function () { + const { agent } = await loadFixture(deployWithAgent); + const { founder } = await getAccounts(); + + const veToken = await ethers.getContractAt("AgentVeToken", agent.veToken); + 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"); + }); - const virtualInfo = await personaNft.virtualInfo(persona.virtualId); - expect(virtualInfo.dao).to.equal(dao); + 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); + }); + + 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 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(await tokenInstance.balanceOf(deployer)).to.equal( - PROPOSAL_THRESHOLD - ); - expect(await tokenInstance.getVotes(deployer)).to.equal(PROPOSAL_THRESHOLD); + expect(virtualInfo.tba).to.equal(agent.tba); }); - 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 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.balanceOf(staker)).to.be.equal(0n); - expect(await tokenInstance.getVotes(validator)).to.be.equal( - PROPOSAL_THRESHOLD - ); - expect(await tokenInstance.getVotes(staker)).to.be.equal(0n); - await demoToken.mint(staker, QUORUM); + const { deployer, poorMan } = await getAccounts(); - const stakeAmount = parseEther("100"); - await demoToken - .connect(this.signers[1]) - .approve(persona.token, stakeAmount); - await tokenInstance - .connect(this.signers[1]) - .stake(stakeAmount, staker, validator); + 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); - 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); - }); + // Now move it + const data = virtualToken.interface.encodeFunctionData("transfer", [ + poorMan.address, + amount, + ]); - 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"); + 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 be able to set new validator and receive delegation", async function () { - const [validator, staker] = this.accounts; - const { persona, demoToken, personaNft } = await loadFixture( - deployGenesisVirtual + 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 ); - const AgentToken = await ethers.getContractFactory("AgentToken"); - const tokenInstance = AgentToken.attach(persona.token); + const agentToken = await ethers.getContractAt("AgentToken", agent.token); + expect(await agentToken.totalSupply()).to.be.equal( + parseEther(process.env.AGENT_TOKEN_LIMIT) + ); + expect(await agentToken.balanceOf(agent.lp)).to.be.equal( + parseEther(process.env.AGENT_TOKEN_LP_SUPPLY) + ); + }); - await demoToken.mint(staker, QUORUM); - const stakeAmount = parseEther("100"); - await demoToken - .connect(this.signers[1]) - .approve(persona.token, stakeAmount); + 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("10000000"); + const capital = parseEther("200000000"); + 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 + 600000) + ); + //// + // 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, parseEther("10000000")); + await virtualToken + .connect(trader) + .approve(process.env.UNISWAP_ROUTER, parseEther("10000000")); + + 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) + ); + ///////////////// + // Staking, and able to delegate to anyone + await lpToken.connect(trader).approve(agent.veToken, parseEther("10")); await expect( - tokenInstance.connect(this.signers[1]).stake(stakeAmount, staker, staker) - ).to.be.revertedWith("Delegatee is not a validator"); - - await personaNft.addValidator(persona.virtualId, staker); + veToken + .connect(trader) + .stake(parseEther("10"), trader.address, poorMan.address) + ).to.be.not.reverted; + }); + 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, + await agentToken.balanceOf(trader.address), + await virtualToken.balanceOf(trader.address), + 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("1000000"); + const capital = parseEther("200000"); + 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) + ); + + 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("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 + ); + }); + + it("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 staked 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 + ); + // 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); + + // 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("1000000"); + const capital = parseEther("2000000"); + 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"); }); });