Skip to content

Commit 5e2bf9e

Browse files
committed
Integration and smoke tests
1 parent 5732fba commit 5e2bf9e

5 files changed

Lines changed: 1199 additions & 0 deletions

File tree

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+
pragma solidity ^0.8.17;
4+
5+
import {Test, Vm} from "forge-std/Test.sol";
6+
import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol";
7+
import {ENSRegistry} from "@ensdomains/ens-contracts/contracts/registry/ENSRegistry.sol";
8+
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
9+
10+
import {DAOFactory} from "../../src/framework/dao/DAOFactory.sol";
11+
import {DAORegistry} from "../../src/framework/dao/DAORegistry.sol";
12+
import {PluginRepoRegistry} from "../../src/framework/plugin/repo/PluginRepoRegistry.sol";
13+
import {PluginRepoFactory} from "../../src/framework/plugin/repo/PluginRepoFactory.sol";
14+
import {PluginRepo} from "../../src/framework/plugin/repo/PluginRepo.sol";
15+
import {PluginSetupProcessor} from "../../src/framework/plugin/setup/PluginSetupProcessor.sol";
16+
import {PluginSetupRef, _getPluginInstallationId} from "../../src/framework/plugin/setup/PluginSetupProcessorHelpers.sol";
17+
import {ENSSubdomainRegistrar} from "../../src/framework/utils/ens/ENSSubdomainRegistrar.sol";
18+
import {DAO} from "../../src/core/dao/DAO.sol";
19+
import {PermissionManager} from "../../src/core/permission/PermissionManager.sol";
20+
import {IDAO} from "../../src/common/dao/IDAO.sol";
21+
import {DAOMock} from "../mocks/commons/dao/DAOMock.sol";
22+
import {MockResolver} from "../framework/member/mocks/MockResolver.sol";
23+
import {PluginUUPSUpgradeableSetupV1Mock} from "../mocks/plugin/UUPSUpgradeable/PluginUUPSUpgradeableSetupMock.sol";
24+
import {PluginUUPSUpgradeableV1Mock} from "../mocks/plugin/UUPSUpgradeable/PluginUUPSUpgradeableMock.sol";
25+
26+
/// @notice System-level invariant tests that compose the full OSx stack —
27+
/// DAOFactory, DAORegistry, PluginRepoRegistry, PluginSetupProcessor,
28+
/// PluginRepoFactory, ENSSubdomainRegistrar — to assert properties that no
29+
/// per-component test could fully verify on its own.
30+
///
31+
/// These are "catastrophic-prevention" checks: each invariant guards against
32+
/// a failure that would compromise every DAO ever created through the
33+
/// factory.
34+
contract CrossComponentInvariantsTest is Test {
35+
bytes32 internal constant ROOT_PERMISSION_ID = keccak256("ROOT_PERMISSION");
36+
bytes32 internal constant UPGRADE_DAO_PERMISSION_ID = keccak256("UPGRADE_DAO_PERMISSION");
37+
bytes32 internal constant SET_TRUSTED_FORWARDER_PERMISSION_ID = keccak256("SET_TRUSTED_FORWARDER_PERMISSION");
38+
bytes32 internal constant SET_METADATA_PERMISSION_ID = keccak256("SET_METADATA_PERMISSION");
39+
bytes32 internal constant REGISTER_STANDARD_CALLBACK_PERMISSION_ID =
40+
keccak256("REGISTER_STANDARD_CALLBACK_PERMISSION");
41+
bytes32 internal constant EXECUTE_PERMISSION_ID = keccak256("EXECUTE_PERMISSION");
42+
bytes32 internal constant APPLY_INSTALLATION_PERMISSION_ID = keccak256("APPLY_INSTALLATION_PERMISSION");
43+
bytes32 internal constant APPLY_UPDATE_PERMISSION_ID = keccak256("APPLY_UPDATE_PERMISSION");
44+
bytes32 internal constant APPLY_UNINSTALLATION_PERMISSION_ID = keccak256("APPLY_UNINSTALLATION_PERMISSION");
45+
46+
address internal constant ANY_ADDR = address(type(uint160).max);
47+
48+
bytes32 internal constant DAO_ETH_NODE = 0x4adec6e9f748b29857b9a275dcb59bd0254a069a7e20cab4ec591499254f119a;
49+
bytes32 internal constant ETH_LABEL = keccak256("eth");
50+
bytes32 internal constant DAO_LABEL = keccak256("dao");
51+
52+
DAOMock internal managingDao;
53+
ENSRegistry internal ens;
54+
MockResolver internal resolver;
55+
ENSSubdomainRegistrar internal subdomainRegistrar;
56+
DAORegistry internal daoRegistry;
57+
PluginRepoRegistry internal pluginRepoRegistry;
58+
PluginSetupProcessor internal psp;
59+
PluginRepoFactory internal pluginRepoFactory;
60+
DAOFactory internal daoFactory;
61+
62+
PluginUUPSUpgradeableSetupV1Mock internal pluginSetupV1Mock;
63+
PluginRepo internal pluginRepo;
64+
65+
function setUp() public {
66+
managingDao = new DAOMock();
67+
managingDao.setHasPermissionReturnValueMock(true);
68+
69+
ens = new ENSRegistry();
70+
resolver = new MockResolver(ENS(address(ens)));
71+
ens.setSubnodeRecord(bytes32(0), ETH_LABEL, address(this), address(resolver), 0);
72+
ens.setSubnodeRecord(
73+
keccak256(abi.encodePacked(bytes32(0), ETH_LABEL)), DAO_LABEL, address(this), address(resolver), 0
74+
);
75+
76+
ENSSubdomainRegistrar registrarImpl = new ENSSubdomainRegistrar();
77+
subdomainRegistrar = ENSSubdomainRegistrar(address(new ERC1967Proxy(address(registrarImpl), "")));
78+
ens.setOwner(DAO_ETH_NODE, address(subdomainRegistrar));
79+
subdomainRegistrar.initialize(IDAO(address(managingDao)), ENS(address(ens)), DAO_ETH_NODE);
80+
81+
DAORegistry daoRegistryImpl = new DAORegistry();
82+
daoRegistry = DAORegistry(
83+
address(
84+
new ERC1967Proxy(
85+
address(daoRegistryImpl),
86+
abi.encodeCall(DAORegistry.initialize, (IDAO(address(managingDao)), subdomainRegistrar))
87+
)
88+
)
89+
);
90+
91+
PluginRepoRegistry pluginRepoRegistryImpl = new PluginRepoRegistry();
92+
pluginRepoRegistry = PluginRepoRegistry(
93+
address(
94+
new ERC1967Proxy(
95+
address(pluginRepoRegistryImpl),
96+
abi.encodeCall(PluginRepoRegistry.initialize, (IDAO(address(managingDao)), subdomainRegistrar))
97+
)
98+
)
99+
);
100+
101+
psp = new PluginSetupProcessor(pluginRepoRegistry);
102+
pluginRepoFactory = new PluginRepoFactory(pluginRepoRegistry);
103+
daoFactory = new DAOFactory(daoRegistry, psp);
104+
105+
PluginUUPSUpgradeableV1Mock pluginImplV1 = new PluginUUPSUpgradeableV1Mock();
106+
pluginSetupV1Mock = new PluginUUPSUpgradeableSetupV1Mock(address(pluginImplV1));
107+
pluginRepo = pluginRepoFactory.createPluginRepoWithFirstVersion(
108+
"plugin-uups-mock", address(pluginSetupV1Mock), address(this), hex"00", hex"00"
109+
);
110+
}
111+
112+
// -------------------------------------------------------------------------
113+
// Helpers
114+
// -------------------------------------------------------------------------
115+
116+
function _defaultSettings(string memory subdomain) internal pure returns (DAOFactory.DAOSettings memory) {
117+
return DAOFactory.DAOSettings({
118+
trustedForwarder: address(0), daoURI: "https://example.org", subdomain: subdomain, metadata: hex"0000"
119+
});
120+
}
121+
122+
function _installationData(uint8 release, uint16 build) internal view returns (DAOFactory.PluginSettings memory) {
123+
return DAOFactory.PluginSettings({
124+
pluginSetupRef: PluginSetupRef({
125+
versionTag: PluginRepo.Tag({release: release, build: build}), pluginSetupRepo: pluginRepo
126+
}),
127+
data: ""
128+
});
129+
}
130+
131+
function _createDaoWithoutPlugins(string memory subdomain) internal returns (DAO) {
132+
(DAO d,) = daoFactory.createDao(_defaultSettings(subdomain), new DAOFactory.PluginSettings[](0));
133+
return d;
134+
}
135+
136+
function _createDaoWithOnePlugin(string memory subdomain) internal returns (DAO) {
137+
DAOFactory.PluginSettings[] memory ps = new DAOFactory.PluginSettings[](1);
138+
ps[0] = _installationData(1, 1);
139+
(DAO d,) = daoFactory.createDao(_defaultSettings(subdomain), ps);
140+
return d;
141+
}
142+
143+
function _createDaoWithOnePluginAndGetAddrs(string memory subdomain)
144+
internal
145+
returns (DAO dao, address plugin)
146+
{
147+
DAOFactory.PluginSettings[] memory ps = new DAOFactory.PluginSettings[](1);
148+
ps[0] = _installationData(1, 1);
149+
DAOFactory.InstalledPlugin[] memory installed;
150+
(dao, installed) = daoFactory.createDao(_defaultSettings(subdomain), ps);
151+
plugin = installed[0].plugin;
152+
}
153+
154+
// -------------------------------------------------------------------------
155+
// O15: catastrophic-prevention — factory + PSP never retain unintended
156+
// permissions on freshly-created DAOs
157+
// -------------------------------------------------------------------------
158+
159+
/// After `createDao` (no plugins), the factory must hold ZERO permissions
160+
/// on the new DAO. Probe each known permission individually — if any
161+
/// future refactor accidentally leaves the factory with ROOT or other
162+
/// permissions, every DAO it creates would be at the factory's mercy.
163+
function test_factoryHasNoPermissionsAfterCreateDao_withoutPlugins() public {
164+
DAO d = _createDaoWithoutPlugins("dao1");
165+
_assertFactoryHasNothing(d);
166+
}
167+
168+
/// Same invariant in the with-plugins branch (which uses additional temp
169+
/// grants that must be revoked at the end).
170+
function test_factoryHasNoPermissionsAfterCreateDao_withPlugins() public {
171+
DAO d = _createDaoWithOnePlugin("dao1");
172+
_assertFactoryHasNothing(d);
173+
}
174+
175+
function _assertFactoryHasNothing(DAO d) internal view {
176+
address f = address(daoFactory);
177+
assertFalse(d.hasPermission(address(d), f, ROOT_PERMISSION_ID, ""), "factory ROOT");
178+
assertFalse(d.hasPermission(address(d), f, UPGRADE_DAO_PERMISSION_ID, ""), "factory UPGRADE_DAO");
179+
assertFalse(d.hasPermission(address(d), f, SET_METADATA_PERMISSION_ID, ""), "factory SET_METADATA");
180+
assertFalse(
181+
d.hasPermission(address(d), f, SET_TRUSTED_FORWARDER_PERMISSION_ID, ""), "factory SET_TRUSTED_FORWARDER"
182+
);
183+
assertFalse(
184+
d.hasPermission(address(d), f, REGISTER_STANDARD_CALLBACK_PERMISSION_ID, ""),
185+
"factory REGISTER_STANDARD_CALLBACK"
186+
);
187+
assertFalse(d.hasPermission(address(d), f, EXECUTE_PERMISSION_ID, ""), "factory EXECUTE");
188+
}
189+
190+
/// PSP only ever receives `ROOT_PERMISSION_ID` on a DAO during plugin
191+
/// install, and that grant is revoked before `createDao` returns. PSP
192+
/// must NEVER hold any other permission on the DAO. Catastrophic if
193+
/// false — PSP holding EXECUTE or UPGRADE_DAO on every freshly-created
194+
/// DAO would let any caller compromise it via the PSP entrypoint.
195+
function test_pspHasNoPermissionsAfterCreateDao_withPlugins() public {
196+
DAO d = _createDaoWithOnePlugin("dao1");
197+
address p = address(psp);
198+
assertFalse(d.hasPermission(address(d), p, ROOT_PERMISSION_ID, ""), "PSP ROOT");
199+
assertFalse(d.hasPermission(address(d), p, UPGRADE_DAO_PERMISSION_ID, ""), "PSP UPGRADE_DAO");
200+
assertFalse(d.hasPermission(address(d), p, EXECUTE_PERMISSION_ID, ""), "PSP EXECUTE");
201+
assertFalse(d.hasPermission(address(d), p, SET_METADATA_PERMISSION_ID, ""), "PSP SET_METADATA");
202+
// The reverse permission (factory holds APPLY_INSTALLATION on PSP)
203+
// is also revoked at the end of the with-plugins branch.
204+
assertFalse(
205+
d.hasPermission(p, address(daoFactory), APPLY_INSTALLATION_PERMISSION_ID, ""),
206+
"factory APPLY_INSTALLATION on PSP"
207+
);
208+
}
209+
210+
// -------------------------------------------------------------------------
211+
// INV-1: permission monotonicity — ROOT and DAO-restricted permissions
212+
// can NEVER be granted to ANY_ADDR under any sequence of grant calls
213+
// -------------------------------------------------------------------------
214+
215+
/// Random fuzz over arbitrary permission ids: every attempt to grant
216+
/// `ROOT_PERMISSION_ID` (or any of the DAO-restricted permissions) to
217+
/// `ANY_ADDR` MUST revert. The invariant holds across the full input
218+
/// space — locks in the PermissionManager guard.
219+
function testFuzz_inv1_rootAndRestrictedNeverGrantableToAnyAddr(bytes32 permissionId) public {
220+
// Fresh DAO; the DAO holds ROOT on itself, so we prank as the DAO.
221+
DAO d = _createDaoWithoutPlugins("inv1-dao");
222+
223+
// Canonical restricted-for-ANY_ADDR set + ROOT.
224+
bytes32[6] memory restricted = [
225+
ROOT_PERMISSION_ID,
226+
EXECUTE_PERMISSION_ID,
227+
UPGRADE_DAO_PERMISSION_ID,
228+
SET_METADATA_PERMISSION_ID,
229+
SET_TRUSTED_FORWARDER_PERMISSION_ID,
230+
REGISTER_STANDARD_CALLBACK_PERMISSION_ID
231+
];
232+
233+
bool isRestricted;
234+
for (uint256 i = 0; i < restricted.length; i++) {
235+
if (permissionId == restricted[i]) {
236+
isRestricted = true;
237+
break;
238+
}
239+
}
240+
241+
if (isRestricted) {
242+
vm.expectRevert(PermissionManager.PermissionsForAnyAddressDisallowed.selector);
243+
vm.prank(address(d));
244+
d.grant(address(d), ANY_ADDR, permissionId);
245+
} else {
246+
vm.prank(address(d));
247+
d.grant(address(d), ANY_ADDR, permissionId);
248+
assertTrue(d.hasPermission(address(d), address(0xBEEF), permissionId, ""));
249+
}
250+
}
251+
252+
// -------------------------------------------------------------------------
253+
// O3: multi-DAO isolation — two independent DAOs share no permission
254+
// state, no registry conflation, no plugin-install state conflation
255+
// -------------------------------------------------------------------------
256+
257+
/// Granting EXECUTE on DAO1 to a third party does NOT grant EXECUTE on
258+
/// DAO2 to the same third party. The permission graphs are fully
259+
/// separate per-DAO.
260+
function test_multiDao_permissionGrantsAreIsolated() public {
261+
DAO dao1 = _createDaoWithoutPlugins("dao-one");
262+
DAO dao2 = _createDaoWithoutPlugins("dao-two");
263+
address operator = makeAddr("operator");
264+
265+
// The `createDao` caller (this contract) receives EXECUTE on DAO1
266+
// and DAO2 (no-plugins branch). To grant on DAO1, this contract
267+
// needs ROOT on DAO1 — but it doesn't (factory revoked own ROOT
268+
// and DAO holds ROOT on itself). Instead, drive the test from the
269+
// DAO's self-ROOT: prank as the DAO.
270+
vm.prank(address(dao1));
271+
dao1.grant(address(dao1), operator, EXECUTE_PERMISSION_ID);
272+
273+
assertTrue(dao1.hasPermission(address(dao1), operator, EXECUTE_PERMISSION_ID, ""), "operator has EXECUTE on dao1");
274+
assertFalse(
275+
dao2.hasPermission(address(dao2), operator, EXECUTE_PERMISSION_ID, ""),
276+
"operator must NOT have EXECUTE on dao2"
277+
);
278+
}
279+
280+
/// `DAORegistry.entries` contains BOTH DAOs after consecutive creates;
281+
/// PluginRepoRegistry is unaffected. Locks in registry scope isolation.
282+
function test_multiDao_registriesScopedCorrectly() public {
283+
DAO dao1 = _createDaoWithoutPlugins("dao-one");
284+
DAO dao2 = _createDaoWithoutPlugins("dao-two");
285+
assertTrue(daoRegistry.entries(address(dao1)));
286+
assertTrue(daoRegistry.entries(address(dao2)));
287+
// PluginRepoRegistry is for plugin repos only — DAOs are not in it.
288+
assertFalse(pluginRepoRegistry.entries(address(dao1)));
289+
assertFalse(pluginRepoRegistry.entries(address(dao2)));
290+
}
291+
292+
/// Two DAOs each get their own plugin proxy AND their own entry in PSP's
293+
/// `states` map (keyed by `(dao, plugin)`). Both installation ids are
294+
/// distinct AND both map to a non-zero `currentAppliedSetupId` —
295+
/// confirming the install state is truly isolated, not just the
296+
/// addresses.
297+
function test_multiDao_pluginInstallStateIsolated() public {
298+
(DAO dao1, address plugin1) = _createDaoWithOnePluginAndGetAddrs("dao-with-plugin-1");
299+
(DAO dao2, address plugin2) = _createDaoWithOnePluginAndGetAddrs("dao-with-plugin-2");
300+
301+
assertTrue(address(dao1) != address(dao2));
302+
assertTrue(plugin1 != plugin2);
303+
304+
bytes32 id1 = _getPluginInstallationId(address(dao1), plugin1);
305+
bytes32 id2 = _getPluginInstallationId(address(dao2), plugin2);
306+
assertTrue(id1 != id2, "installation ids distinct");
307+
308+
(uint256 block1, bytes32 applied1) = psp.states(id1);
309+
(uint256 block2, bytes32 applied2) = psp.states(id2);
310+
assertTrue(block1 != 0 && applied1 != bytes32(0), "dao1 install latched");
311+
assertTrue(block2 != 0 && applied2 != bytes32(0), "dao2 install latched");
312+
313+
// Cross-probe: dao1's installationId for dao2's plugin (and vice
314+
// versa) must NOT exist in PSP.states. Confirms keyed-by-(dao, plugin)
315+
// — no cross-DAO conflation even when both repos / setups match.
316+
bytes32 cross1 = _getPluginInstallationId(address(dao1), plugin2);
317+
bytes32 cross2 = _getPluginInstallationId(address(dao2), plugin1);
318+
(uint256 cBlock1, bytes32 cApplied1) = psp.states(cross1);
319+
(uint256 cBlock2, bytes32 cApplied2) = psp.states(cross2);
320+
assertEq(cBlock1, 0, "no cross-DAO entry for (dao1, plugin2)");
321+
assertEq(cApplied1, bytes32(0));
322+
assertEq(cBlock2, 0, "no cross-DAO entry for (dao2, plugin1)");
323+
assertEq(cApplied2, bytes32(0));
324+
}
325+
}

0 commit comments

Comments
 (0)