diff --git a/src/contracts/Plugin.sol b/src/contracts/Plugin.sol index a29b9f8b..3230df2a 100644 --- a/src/contracts/Plugin.sol +++ b/src/contracts/Plugin.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.25; import {IPlugin} from "src/interfaces/IPlugin.sol"; import {IFactory} from "src/interfaces/IFactory.sol"; -contract Plugin is IPlugin { +abstract contract Plugin is IPlugin { /** * @inheritdoc IPlugin */ diff --git a/src/contracts/Vault.sol b/src/contracts/Vault.sol index 1e5b1f5d..d8f7c1cb 100644 --- a/src/contracts/Vault.sol +++ b/src/contracts/Vault.sol @@ -661,6 +661,10 @@ contract Vault is MigratableEntity, ERC6372, ReentrancyGuardUpgradeable, AccessC revert NetworkAlreadyOptedIn(); } + if (maxNetworkLimit_ == 0) { + revert InvalidMaxNetworkLimit(); + } + isNetworkOptedIn[msg.sender][resolver] = true; _networkLimit[msg.sender][resolver].amount = 0; diff --git a/src/interfaces/IVault.sol b/src/interfaces/IVault.sol index 6b3f75ce..79260077 100644 --- a/src/interfaces/IVault.sol +++ b/src/interfaces/IVault.sol @@ -25,6 +25,7 @@ interface IVault { error NotResolver(); error VetoPeriodEnded(); error NetworkAlreadyOptedIn(); + error InvalidMaxNetworkLimit(); error NetworkNotOptedIn(); error OperatorAlreadyOptedIn(); error ExceedsMaxNetworkLimit(); diff --git a/test/NonMigratablesRegistry.sol b/test/NonMigratablesRegistry.sol new file mode 100644 index 00000000..6b433b0c --- /dev/null +++ b/test/NonMigratablesRegistry.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Test, console2} from "forge-std/Test.sol"; + +import {NonMigratablesRegistry} from "src/contracts/NonMigratablesRegistry.sol"; +import {INonMigratablesRegistry} from "src/interfaces/INonMigratablesRegistry.sol"; + +contract NonMigratablesRegistryTest is Test { + address owner; + address alice; + uint256 alicePrivateKey; + address bob; + uint256 bobPrivateKey; + + INonMigratablesRegistry registry; + + function setUp() public { + owner = address(this); + (alice, alicePrivateKey) = makeAddrAndKey("alice"); + (bob, bobPrivateKey) = makeAddrAndKey("bob"); + } + + function test_Create() public { + registry = new NonMigratablesRegistry(); + + assertEq(registry.isEntity(alice), false); + } + + function test_Register() public { + registry = new NonMigratablesRegistry(); + + vm.startPrank(alice); + registry.register(); + vm.stopPrank(); + + assertEq(registry.isEntity(alice), true); + } +} diff --git a/test/Plugin.t.sol b/test/Plugin.t.sol new file mode 100644 index 00000000..e8395c62 --- /dev/null +++ b/test/Plugin.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Test, console2} from "forge-std/Test.sol"; + +import {NonMigratablesRegistry} from "src/contracts/NonMigratablesRegistry.sol"; +import {IPlugin} from "src/interfaces/IPlugin.sol"; + +import {SimplePlugin} from "./mocks/SimplePlugin.sol"; + +contract PluginTest is Test { + address owner; + address alice; + uint256 alicePrivateKey; + address bob; + uint256 bobPrivateKey; + + NonMigratablesRegistry registry; + + SimplePlugin plugin; + + function setUp() public { + owner = address(this); + (alice, alicePrivateKey) = makeAddrAndKey("alice"); + (bob, bobPrivateKey) = makeAddrAndKey("bob"); + + registry = new NonMigratablesRegistry(); + } + + function test_Create(uint256 number) public { + plugin = new SimplePlugin(address(registry)); + + assertEq(plugin.REGISTRY(), address(registry)); + assertEq(plugin.number(alice), 0); + + vm.startPrank(alice); + registry.register(); + vm.stopPrank(); + + vm.startPrank(alice); + plugin.setNumber(number); + vm.stopPrank(); + + assertEq(plugin.number(alice), number); + } + + function test_SetNumberRevertNotEntity(uint256 number) public { + plugin = new SimplePlugin(address(registry)); + + vm.startPrank(alice); + vm.expectRevert(IPlugin.NotEntity.selector); + plugin.setNumber(number); + vm.stopPrank(); + } +} diff --git a/test/Vault.t.sol b/test/Vault.t.sol index 0309e029..501885d1 100644 --- a/test/Vault.t.sol +++ b/test/Vault.t.sol @@ -135,6 +135,7 @@ contract VaultTest is Test { assertEq(vault.isNetworkOptedIn(address(0), address(0)), false); assertEq(vault.isOperatorOptedIn(address(0)), false); assertEq(vault.operatorOptOutAt(address(0)), 0); + assertEq(vault.maxNetworkLimit(address(0), address(0)), 0); assertEq(vault.networkLimit(address(0), address(0)), 0); (uint256 nextNetworkLimitAmount, uint256 nextNetworkLimitTimestamp) = vault.nextNetworkLimit(address(0), address(0)); @@ -1655,7 +1656,10 @@ contract VaultTest is Test { ); } - function test_OptInNetwork() public { + function test_OptInNetwork(uint256 networkLimit, uint256 maxNetworkLimit) public { + maxNetworkLimit = bound(maxNetworkLimit, 1, type(uint256).max); + vm.assume(networkLimit <= maxNetworkLimit); + string memory metadataURL = ""; uint48 epochDuration = 1; uint48 slashDuration = 1; @@ -1668,17 +1672,19 @@ contract VaultTest is Test { _registerNetwork(network, bob); address resolver = address(1); - _optInNetwork(network, resolver, type(uint256).max); + _optInNetwork(network, resolver, maxNetworkLimit); assertTrue(vault.isNetworkOptedIn(network, resolver)); (uint256 nextNetworkLimitAmount, uint256 nextNetworkLimitTimestamp) = vault.nextNetworkLimit(network, resolver); assertEq(nextNetworkLimitAmount, 0); assertEq(nextNetworkLimitTimestamp, 0); assertEq(vault.networkLimit(network, resolver), 0); + assertEq(vault.maxNetworkLimit(network, resolver), maxNetworkLimit); - _setNetworkLimit(alice, network, resolver, 5); + _setNetworkLimit(alice, network, resolver, networkLimit); - assertEq(vault.networkLimit(network, resolver), 5); + assertEq(vault.networkLimit(network, resolver), networkLimit); + assertEq(vault.maxNetworkLimit(network, resolver), maxNetworkLimit); blockTimestamp = blockTimestamp + 1; vm.warp(blockTimestamp); @@ -1689,7 +1695,8 @@ contract VaultTest is Test { (nextNetworkLimitAmount, nextNetworkLimitTimestamp) = vault.nextNetworkLimit(network, resolver); assertEq(nextNetworkLimitAmount, 0); assertEq(nextNetworkLimitTimestamp, vault.currentEpochStart() + 2 * vault.epochDuration()); - assertEq(vault.networkLimit(network, resolver), 5); + assertEq(vault.networkLimit(network, resolver), networkLimit); + assertEq(vault.maxNetworkLimit(network, resolver), 0); blockTimestamp = blockTimestamp + 2; vm.warp(blockTimestamp); @@ -1698,6 +1705,7 @@ contract VaultTest is Test { assertEq(nextNetworkLimitAmount, 0); assertEq(nextNetworkLimitTimestamp, vault.currentEpochStart()); assertEq(vault.networkLimit(network, resolver), 0); + assertEq(vault.maxNetworkLimit(network, resolver), 0); _optInNetwork(network, resolver, type(uint256).max); @@ -1706,6 +1714,7 @@ contract VaultTest is Test { assertEq(nextNetworkLimitAmount, 0); assertEq(nextNetworkLimitTimestamp, 0); assertEq(vault.networkLimit(network, resolver), 0); + assertEq(vault.maxNetworkLimit(network, resolver), type(uint256).max); blockTimestamp = blockTimestamp + 3; vm.warp(blockTimestamp); @@ -1714,6 +1723,7 @@ contract VaultTest is Test { assertEq(nextNetworkLimitAmount, 0); assertEq(nextNetworkLimitTimestamp, 0); assertEq(vault.networkLimit(network, resolver), 0); + assertEq(vault.maxNetworkLimit(network, resolver), type(uint256).max); _optOutNetwork(network, resolver); @@ -1722,6 +1732,22 @@ contract VaultTest is Test { assertEq(nextNetworkLimitAmount, 0); assertEq(nextNetworkLimitTimestamp, vault.currentEpochStart() + 2 * vault.epochDuration()); assertEq(vault.networkLimit(network, resolver), 0); + assertEq(vault.maxNetworkLimit(network, resolver), 0); + } + + function test_OptInNetworkRevertInvalidMaxNetworkLimit() public { + string memory metadataURL = ""; + uint48 epochDuration = 1; + uint48 slashDuration = 1; + uint48 vetoDuration = 0; + vault = _getVault(metadataURL, epochDuration, slashDuration, vetoDuration); + + address network = bob; + _registerNetwork(network, bob); + + address resolver = address(1); + vm.expectRevert(IVault.InvalidMaxNetworkLimit.selector); + _optInNetwork(network, resolver, 0); } function test_OptInNetworkRevertNetworkAlreadyOptedIn() public { @@ -2025,7 +2051,33 @@ contract VaultTest is Test { _setNetworkLimit(alice, network, resolver, amount1); } - function test_SetOperatorLimit1(uint48 epochDuration, uint256 amount1, uint256 amount2, uint256 amount3) public { + function test_SetNetworkLimitRevertExceedsMaxNetworkLimit( + uint48 epochDuration, + uint256 amount1, + uint256 maxNetworkLimit + ) public { + epochDuration = uint48(bound(uint256(epochDuration), 1, 100 days)); + maxNetworkLimit = bound(maxNetworkLimit, 1, type(uint256).max); + vm.assume(amount1 > maxNetworkLimit); + + string memory metadataURL = ""; + uint48 slashDuration = 1; + uint48 vetoDuration = 0; + vault = _getVault(metadataURL, epochDuration, slashDuration, vetoDuration); + + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp; + + address network = bob; + _registerNetwork(network, bob); + + address resolver = address(1); + _optInNetwork(network, resolver, maxNetworkLimit); + + vm.expectRevert(IVault.ExceedsMaxNetworkLimit.selector); + _setNetworkLimit(alice, network, resolver, amount1); + } + + function test_SetOperatorLimit(uint48 epochDuration, uint256 amount1, uint256 amount2, uint256 amount3) public { epochDuration = uint48(bound(uint256(epochDuration), 1, 100 days)); vm.assume(amount3 < amount2); @@ -2507,6 +2559,48 @@ contract VaultTest is Test { _claimRewards(alice, rewardClaims); } + function test_ClaimRewardsRevertInvalidHintsLength(uint256 amount, uint256 ditributeAmount) public { + amount = bound(amount, 1, 100 * 10 ** 18); + ditributeAmount = bound(ditributeAmount, 1, 100 * 10 ** 18); + + string memory metadataURL = ""; + uint48 epochDuration = 1; + uint48 slashDuration = 1; + uint48 vetoDuration = 0; + vault = _getVault(metadataURL, epochDuration, slashDuration, vetoDuration); + + address network = bob; + _registerNetwork(network, bob); + + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp; + + for (uint256 i; i < 10; ++i) { + _deposit(alice, amount); + + blockTimestamp = blockTimestamp + 1; + vm.warp(blockTimestamp); + } + + IERC20 token = IERC20(new Token("Token")); + token.transfer(bob, 100_000 * 1e18); + vm.startPrank(bob); + token.approve(address(vault), type(uint256).max); + vm.stopPrank(); + + uint48 timestamp = 3; + _ditributeReward(bob, network, address(token), ditributeAmount, timestamp); + + IVault.RewardClaim[] memory rewardClaims = new IVault.RewardClaim[](1); + rewardClaims[0] = IVault.RewardClaim({ + token: address(token), + amountIndexes: type(uint256).max, + activeSharesOfHints: new uint32[](2) + }); + + vm.expectRevert(IVault.InvalidHintsLength.selector); + _claimRewards(alice, rewardClaims); + } + function test_SetHasDepositWhitelist() public { string memory metadataURL = ""; uint48 epochDuration = 1; diff --git a/test/mocks/SimplePlugin.sol b/test/mocks/SimplePlugin.sol new file mode 100644 index 00000000..cc90c56b --- /dev/null +++ b/test/mocks/SimplePlugin.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Plugin} from "src/contracts/Plugin.sol"; + +contract SimplePlugin is Plugin { + mapping(address entity => uint256 value) public number; + + constructor(address registry) Plugin(registry) {} + + function setNumber(uint256 number_) external onlyEntity { + number[msg.sender] = number_; + } +} diff --git a/test/plugins/MetadataPlugin.t.sol b/test/plugins/MetadataPlugin.t.sol new file mode 100644 index 00000000..8e8c945d --- /dev/null +++ b/test/plugins/MetadataPlugin.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Test, console2} from "forge-std/Test.sol"; + +import {NonMigratablesRegistry} from "src/contracts/NonMigratablesRegistry.sol"; +import {IPlugin} from "src/interfaces/IPlugin.sol"; + +import {MetadataPlugin} from "src/contracts/plugins/MetadataPlugin.sol"; +import {IMetadataPlugin} from "src/interfaces/plugins/IMetadataPlugin.sol"; + +contract MetadataPluginTest is Test { + address owner; + address alice; + uint256 alicePrivateKey; + address bob; + uint256 bobPrivateKey; + + NonMigratablesRegistry registry; + + IMetadataPlugin plugin; + + function setUp() public { + owner = address(this); + (alice, alicePrivateKey) = makeAddrAndKey("alice"); + (bob, bobPrivateKey) = makeAddrAndKey("bob"); + + registry = new NonMigratablesRegistry(); + } + + function test_Create(string calldata metadataURL_) public { + plugin = IMetadataPlugin(address(new MetadataPlugin(address(registry)))); + + assertEq(plugin.metadataURL(alice), ""); + + vm.startPrank(alice); + registry.register(); + vm.stopPrank(); + + vm.startPrank(alice); + plugin.setMetadataURL(metadataURL_); + vm.stopPrank(); + + assertEq(plugin.metadataURL(alice), metadataURL_); + } + + function test_SetNumberRevertNotEntity(string calldata metadataURL_) public { + plugin = IMetadataPlugin(address(new MetadataPlugin(address(registry)))); + + vm.startPrank(alice); + vm.expectRevert(IPlugin.NotEntity.selector); + plugin.setMetadataURL(metadataURL_); + vm.stopPrank(); + } +} diff --git a/test/plugins/MiddlewarePlugin.t.sol b/test/plugins/MiddlewarePlugin.t.sol new file mode 100644 index 00000000..e4193992 --- /dev/null +++ b/test/plugins/MiddlewarePlugin.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Test, console2} from "forge-std/Test.sol"; + +import {NonMigratablesRegistry} from "src/contracts/NonMigratablesRegistry.sol"; +import {IPlugin} from "src/interfaces/IPlugin.sol"; + +import {MiddlewarePlugin} from "src/contracts/plugins/MiddlewarePlugin.sol"; +import {IMiddlewarePlugin} from "src/interfaces/plugins/IMiddlewarePlugin.sol"; + +contract MiddlewarePluginTest is Test { + address owner; + address alice; + uint256 alicePrivateKey; + address bob; + uint256 bobPrivateKey; + + NonMigratablesRegistry registry; + + IMiddlewarePlugin plugin; + + function setUp() public { + owner = address(this); + (alice, alicePrivateKey) = makeAddrAndKey("alice"); + (bob, bobPrivateKey) = makeAddrAndKey("bob"); + + registry = new NonMigratablesRegistry(); + } + + function test_Create(address middleware) public { + plugin = IMiddlewarePlugin(address(new MiddlewarePlugin(address(registry)))); + + assertEq(plugin.middleware(alice), address(0)); + + vm.startPrank(alice); + registry.register(); + vm.stopPrank(); + + vm.startPrank(alice); + plugin.setMiddleware(middleware); + vm.stopPrank(); + + assertEq(plugin.middleware(alice), middleware); + } + + function test_SetNumberRevertNotEntity(address middleware) public { + plugin = IMiddlewarePlugin(address(new MiddlewarePlugin(address(registry)))); + + vm.startPrank(alice); + vm.expectRevert(IPlugin.NotEntity.selector); + plugin.setMiddleware(middleware); + vm.stopPrank(); + } +} diff --git a/test/plugins/NetworkOptInPlugin.t.sol b/test/plugins/NetworkOptInPlugin.t.sol new file mode 100644 index 00000000..57ff9ce2 --- /dev/null +++ b/test/plugins/NetworkOptInPlugin.t.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Test, console2} from "forge-std/Test.sol"; + +import {NonMigratablesRegistry} from "src/contracts/NonMigratablesRegistry.sol"; +import {IPlugin} from "src/interfaces/IPlugin.sol"; + +import {NetworkOptInPlugin} from "src/contracts/plugins/NetworkOptInPlugin.sol"; +import {INetworkOptInPlugin} from "src/interfaces/plugins/INetworkOptInPlugin.sol"; + +contract NetworkOptInPluginTest is Test { + address owner; + address alice; + uint256 alicePrivateKey; + address bob; + uint256 bobPrivateKey; + + NonMigratablesRegistry operatorRegistry; + NonMigratablesRegistry networkRegistry; + + INetworkOptInPlugin plugin; + + function setUp() public { + owner = address(this); + (alice, alicePrivateKey) = makeAddrAndKey("alice"); + (bob, bobPrivateKey) = makeAddrAndKey("bob"); + + operatorRegistry = new NonMigratablesRegistry(); + networkRegistry = new NonMigratablesRegistry(); + } + + function test_Create(address middleware) public { + plugin = + INetworkOptInPlugin(address(new NetworkOptInPlugin(address(operatorRegistry), address(networkRegistry)))); + + assertEq(plugin.NETWORK_REGISTRY(), address(networkRegistry)); + assertEq(plugin.isOperatorOptedIn(alice, alice), false); + assertEq(plugin.lastOperatorOptOut(alice, alice), 0); + + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp; + + address operator = alice; + address network = bob; + + vm.startPrank(operator); + operatorRegistry.register(); + vm.stopPrank(); + + vm.startPrank(network); + networkRegistry.register(); + vm.stopPrank(); + + vm.startPrank(operator); + plugin.optIn(network); + vm.stopPrank(); + + assertEq(plugin.isOperatorOptedIn(operator, network), true); + assertEq(plugin.lastOperatorOptOut(operator, network), 0); + + blockTimestamp = blockTimestamp + 1; + vm.warp(blockTimestamp); + + assertEq(plugin.isOperatorOptedIn(operator, network), true); + assertEq(plugin.lastOperatorOptOut(operator, network), 0); + + vm.startPrank(operator); + plugin.optOut(network); + vm.stopPrank(); + + assertEq(plugin.isOperatorOptedIn(operator, network), false); + assertEq(plugin.lastOperatorOptOut(operator, network), blockTimestamp); + + blockTimestamp = blockTimestamp + 1; + vm.warp(blockTimestamp); + + assertEq(plugin.isOperatorOptedIn(operator, network), false); + assertEq(plugin.lastOperatorOptOut(operator, network), blockTimestamp - 1); + + vm.startPrank(operator); + plugin.optIn(network); + vm.stopPrank(); + + assertEq(plugin.isOperatorOptedIn(operator, network), true); + assertEq(plugin.lastOperatorOptOut(operator, network), blockTimestamp - 1); + + vm.startPrank(operator); + plugin.optOut(network); + vm.stopPrank(); + + assertEq(plugin.isOperatorOptedIn(operator, network), false); + assertEq(plugin.lastOperatorOptOut(operator, network), blockTimestamp); + } + + function test_SetNumberRevertNotEntity(address middleware) public { + plugin = + INetworkOptInPlugin(address(new NetworkOptInPlugin(address(operatorRegistry), address(networkRegistry)))); + + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp; + + address operator = alice; + address network = bob; + + vm.startPrank(network); + networkRegistry.register(); + vm.stopPrank(); + + vm.startPrank(operator); + vm.expectRevert(IPlugin.NotEntity.selector); + plugin.optIn(network); + vm.stopPrank(); + } + + function test_SetNumberRevertNotNetwork(address middleware) public { + plugin = + INetworkOptInPlugin(address(new NetworkOptInPlugin(address(operatorRegistry), address(networkRegistry)))); + + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp; + + address operator = alice; + address network = bob; + + vm.startPrank(operator); + operatorRegistry.register(); + vm.stopPrank(); + + vm.startPrank(operator); + vm.expectRevert(INetworkOptInPlugin.NotNetwork.selector); + plugin.optIn(network); + vm.stopPrank(); + } + + function test_SetNumberRevertOperatorAlreadyOptedIn(address middleware) public { + plugin = + INetworkOptInPlugin(address(new NetworkOptInPlugin(address(operatorRegistry), address(networkRegistry)))); + + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp; + + address operator = alice; + address network = bob; + + vm.startPrank(operator); + operatorRegistry.register(); + vm.stopPrank(); + + vm.startPrank(network); + networkRegistry.register(); + vm.stopPrank(); + + vm.startPrank(operator); + plugin.optIn(network); + vm.stopPrank(); + + vm.startPrank(operator); + vm.expectRevert(INetworkOptInPlugin.OperatorAlreadyOptedIn.selector); + plugin.optIn(network); + vm.stopPrank(); + } + + function test_SetNumberRevertOperatorNotOptedIn(address middleware) public { + plugin = + INetworkOptInPlugin(address(new NetworkOptInPlugin(address(operatorRegistry), address(networkRegistry)))); + + uint256 blockTimestamp = block.timestamp * block.timestamp / block.timestamp; + + address operator = alice; + address network = bob; + + vm.startPrank(operator); + operatorRegistry.register(); + vm.stopPrank(); + + vm.startPrank(network); + networkRegistry.register(); + vm.stopPrank(); + + vm.startPrank(operator); + vm.expectRevert(INetworkOptInPlugin.OperatorNotOptedIn.selector); + plugin.optOut(network); + vm.stopPrank(); + } +}