326 lines
12 KiB
Solidity
326 lines
12 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "forge-std/Test.sol";
|
|
import "../../contracts/registry/UniversalAssetRegistry.sol";
|
|
import "../../contracts/governance/GovernanceController.sol";
|
|
import "../../contracts/tokens/CompliantFiatTokenV2.sol";
|
|
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
|
|
|
contract JurisdictionalGovernanceTest is Test {
|
|
UniversalAssetRegistry internal registry;
|
|
GovernanceController internal governance;
|
|
CompliantFiatTokenV2 internal token;
|
|
|
|
address internal admin;
|
|
address internal authority;
|
|
address internal transitionAuthority;
|
|
|
|
function setUp() public {
|
|
admin = makeAddr("admin");
|
|
authority = makeAddr("authority");
|
|
transitionAuthority = makeAddr("transitionAuthority");
|
|
|
|
vm.startPrank(admin);
|
|
|
|
UniversalAssetRegistry registryImpl = new UniversalAssetRegistry();
|
|
bytes memory registryInit = abi.encodeCall(UniversalAssetRegistry.initialize, (admin));
|
|
ERC1967Proxy registryProxy = new ERC1967Proxy(address(registryImpl), registryInit);
|
|
registry = UniversalAssetRegistry(address(registryProxy));
|
|
|
|
GovernanceController governanceImpl = new GovernanceController();
|
|
bytes memory governanceInit = abi.encodeCall(GovernanceController.initialize, (address(registry), admin));
|
|
ERC1967Proxy governanceProxy = new ERC1967Proxy(address(governanceImpl), governanceInit);
|
|
governance = GovernanceController(address(governanceProxy));
|
|
|
|
token = new CompliantFiatTokenV2(
|
|
"Euro Coin (Compliant V2)",
|
|
"cEURC",
|
|
6,
|
|
"EUR",
|
|
"2",
|
|
admin,
|
|
admin,
|
|
0,
|
|
true
|
|
);
|
|
|
|
registry.setGovernanceController(address(governance));
|
|
token.setGovernanceController(address(governance));
|
|
token.emergencySetGovernanceMetadata(
|
|
token.governanceProfileId(),
|
|
token.supervisionProfileId(),
|
|
token.storageNamespace(),
|
|
"EU",
|
|
address(0),
|
|
true,
|
|
true,
|
|
14 days
|
|
);
|
|
registry.emergencySetJurisdictionProfile(
|
|
"EU",
|
|
true,
|
|
true,
|
|
true,
|
|
14 days,
|
|
"ipfs://eu-supervision",
|
|
keccak256("eu-policy")
|
|
);
|
|
registry.emergencySetJurisdictionProfile(
|
|
"SG",
|
|
true,
|
|
true,
|
|
true,
|
|
21 days,
|
|
"ipfs://sg-supervision",
|
|
keccak256("sg-policy")
|
|
);
|
|
registry.emergencySetJurisdictionAuthority("EU", authority, true, true, true, true, true);
|
|
registry.emergencySetJurisdictionAuthority("SG", transitionAuthority, true, true, true, true, true);
|
|
registry.addValidator(authority);
|
|
registry.registerGRUCompliantAsset(address(token), "Euro Coin (Compliant V2)", "cEURC", 6, "EU");
|
|
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function testRegistryPullsGovernanceMetadataFromTokenAndJurisdiction() public view {
|
|
UniversalAssetRegistry.UniversalAsset memory asset = registry.getAsset(address(token));
|
|
|
|
assertEq(asset.jurisdiction, "EU");
|
|
assertEq(asset.assetId, token.assetId());
|
|
assertEq(asset.assetVersionId, token.assetVersionId());
|
|
assertEq(asset.governanceProfileId, token.governanceProfileId());
|
|
assertEq(asset.supervisionProfileId, token.supervisionProfileId());
|
|
assertEq(asset.storageNamespace, token.storageNamespace());
|
|
assertTrue(asset.supervisionRequired);
|
|
assertTrue(asset.governmentApprovalRequired);
|
|
assertEq(asset.minimumUpgradeNoticePeriod, 14 days);
|
|
}
|
|
|
|
function testQueueRequiresJurisdictionApprovalAndUsesJurisdictionNoticePeriod() public {
|
|
address[] memory targets = new address[](1);
|
|
uint256[] memory values = new uint256[](1);
|
|
bytes[] memory calldatas = new bytes[](1);
|
|
bytes32 updatedGovernanceProfile = keccak256("eu-governance-v2");
|
|
|
|
targets[0] = address(token);
|
|
values[0] = 0;
|
|
calldatas[0] = abi.encodeWithSelector(
|
|
CompliantFiatTokenV2.setGovernanceProfileId.selector,
|
|
updatedGovernanceProfile
|
|
);
|
|
|
|
vm.prank(admin);
|
|
uint256 proposalId = governance.proposeForAsset(
|
|
address(token),
|
|
targets,
|
|
values,
|
|
calldatas,
|
|
"Update EU governance metadata",
|
|
GovernanceController.GovernanceMode.TimelockShort
|
|
);
|
|
assertEq(governance.proposalAssets(proposalId), address(token));
|
|
|
|
vm.roll(block.number + governance.votingDelay() + 1);
|
|
|
|
vm.prank(authority);
|
|
governance.castVote(proposalId, 1);
|
|
|
|
vm.roll(block.number + governance.votingPeriod() + 1);
|
|
|
|
vm.expectRevert("Primary jurisdiction approval required");
|
|
governance.queue(proposalId);
|
|
|
|
vm.prank(authority);
|
|
governance.approveProposalJurisdiction(proposalId);
|
|
|
|
uint256 beforeQueue = block.timestamp;
|
|
governance.queue(proposalId);
|
|
|
|
uint256 eta = governance.getProposalEta(proposalId);
|
|
assertGe(eta, beforeQueue + 14 days);
|
|
assertEq(governance.proposalJurisdictionApprovalCount(proposalId), 1);
|
|
assertEq(governance.proposalLastJurisdictionApprover(proposalId), authority);
|
|
|
|
vm.warp(eta);
|
|
governance.execute(proposalId);
|
|
|
|
assertEq(token.governanceProfileId(), updatedGovernanceProfile);
|
|
}
|
|
|
|
function testAssetScopedProposalRejectsRegistryCallsForAnotherAsset() public {
|
|
address[] memory targets = new address[](1);
|
|
uint256[] memory values = new uint256[](1);
|
|
bytes[] memory calldatas = new bytes[](1);
|
|
|
|
targets[0] = address(registry);
|
|
values[0] = 0;
|
|
calldatas[0] = abi.encodeWithSelector(
|
|
UniversalAssetRegistry.syncAssetMetadataFromToken.selector,
|
|
address(0xBEEF)
|
|
);
|
|
|
|
vm.prank(admin);
|
|
vm.expectRevert("Registry asset mismatch");
|
|
governance.proposeForAsset(
|
|
address(token),
|
|
targets,
|
|
values,
|
|
calldatas,
|
|
"Attempt mismatched registry sync",
|
|
GovernanceController.GovernanceMode.TimelockShort
|
|
);
|
|
}
|
|
|
|
function testRegistryMetadataRequiresGovernanceButEmergencyPathRemainsAvailable() public {
|
|
vm.prank(admin);
|
|
vm.expectRevert();
|
|
registry.setAssetGovernanceProfile(
|
|
address(token),
|
|
keccak256("blocked-gov-profile"),
|
|
keccak256("blocked-sup-profile"),
|
|
keccak256("blocked-storage"),
|
|
address(0x1234),
|
|
false,
|
|
true,
|
|
true,
|
|
21 days,
|
|
"ipfs://blocked-disclosure",
|
|
"ipfs://blocked-reporting"
|
|
);
|
|
|
|
vm.prank(admin);
|
|
registry.emergencySetAssetGovernanceProfile(
|
|
address(token),
|
|
keccak256("manual-gov-profile"),
|
|
keccak256("manual-sup-profile"),
|
|
keccak256("manual-storage"),
|
|
address(0x1234),
|
|
false,
|
|
true,
|
|
true,
|
|
21 days,
|
|
"ipfs://manual-disclosure",
|
|
"ipfs://manual-reporting"
|
|
);
|
|
|
|
UniversalAssetRegistry.UniversalAsset memory asset = registry.getAsset(address(token));
|
|
assertEq(asset.governanceProfileId, keccak256("manual-gov-profile"));
|
|
assertEq(asset.supervisionProfileId, keccak256("manual-sup-profile"));
|
|
assertEq(asset.storageNamespace, keccak256("manual-storage"));
|
|
assertEq(asset.canonicalUnderlyingAsset, address(0x1234));
|
|
assertEq(asset.regulatoryDisclosureURI, "ipfs://manual-disclosure");
|
|
assertEq(asset.reportingURI, "ipfs://manual-reporting");
|
|
assertEq(asset.minimumUpgradeNoticePeriod, 21 days);
|
|
}
|
|
|
|
function testJurisdictionProfileCanBeManagedThroughAssetScopedGovernance() public {
|
|
address[] memory targets = new address[](1);
|
|
uint256[] memory values = new uint256[](1);
|
|
bytes[] memory calldatas = new bytes[](1);
|
|
|
|
targets[0] = address(registry);
|
|
values[0] = 0;
|
|
calldatas[0] = abi.encodeWithSelector(
|
|
UniversalAssetRegistry.setDerivedJurisdictionProfile.selector,
|
|
address(token),
|
|
true,
|
|
true,
|
|
true,
|
|
30 days,
|
|
"ipfs://eu-supervision-v2",
|
|
keccak256("eu-policy-v2")
|
|
);
|
|
|
|
vm.prank(admin);
|
|
uint256 proposalId = governance.proposeForAsset(
|
|
address(token),
|
|
targets,
|
|
values,
|
|
calldatas,
|
|
"Update derived EU jurisdiction profile",
|
|
GovernanceController.GovernanceMode.TimelockShort
|
|
);
|
|
|
|
vm.roll(block.number + governance.votingDelay() + 1);
|
|
vm.prank(authority);
|
|
governance.castVote(proposalId, 1);
|
|
vm.roll(block.number + governance.votingPeriod() + 1);
|
|
|
|
vm.prank(authority);
|
|
governance.approveProposalJurisdiction(proposalId);
|
|
|
|
governance.queue(proposalId);
|
|
vm.warp(governance.getProposalEta(proposalId));
|
|
governance.execute(proposalId);
|
|
|
|
UniversalAssetRegistry.JurisdictionProfile memory profile = registry.getJurisdictionProfile("EU");
|
|
assertEq(profile.minimumUpgradeNoticePeriod, 30 days);
|
|
assertEq(profile.supervisionURI, "ipfs://eu-supervision-v2");
|
|
assertEq(profile.policyHash, keccak256("eu-policy-v2"));
|
|
}
|
|
|
|
function testJurisdictionTransitionRequiresCurrentAndDestinationApprovals() public {
|
|
address[] memory targets = new address[](2);
|
|
uint256[] memory values = new uint256[](2);
|
|
bytes[] memory calldatas = new bytes[](2);
|
|
|
|
targets[0] = address(token);
|
|
values[0] = 0;
|
|
calldatas[0] = abi.encodeWithSelector(
|
|
CompliantFiatTokenV2.setPrimaryJurisdiction.selector,
|
|
"SG"
|
|
);
|
|
|
|
targets[1] = address(registry);
|
|
values[1] = 0;
|
|
calldatas[1] = abi.encodeWithSelector(
|
|
UniversalAssetRegistry.syncAssetMetadataFromToken.selector,
|
|
address(token)
|
|
);
|
|
|
|
vm.prank(admin);
|
|
uint256 proposalId = governance.proposeForAsset(
|
|
address(token),
|
|
targets,
|
|
values,
|
|
calldatas,
|
|
"Move asset jurisdiction to SG",
|
|
GovernanceController.GovernanceMode.TimelockShort
|
|
);
|
|
|
|
assertEq(governance.proposalTransitionJurisdictionIds(proposalId), registry.jurisdictionIdFor("SG"));
|
|
|
|
vm.roll(block.number + governance.votingDelay() + 1);
|
|
vm.prank(authority);
|
|
governance.castVote(proposalId, 1);
|
|
vm.roll(block.number + governance.votingPeriod() + 1);
|
|
|
|
vm.prank(authority);
|
|
governance.approveProposalJurisdiction(proposalId);
|
|
|
|
vm.expectRevert("Transition jurisdiction approval required");
|
|
governance.queue(proposalId);
|
|
|
|
vm.prank(transitionAuthority);
|
|
governance.approveProposalJurisdiction(proposalId);
|
|
|
|
uint256 beforeQueue = block.timestamp;
|
|
governance.queue(proposalId);
|
|
|
|
assertTrue(governance.hasJurisdictionApproval(proposalId, authority));
|
|
assertTrue(governance.hasTransitionJurisdictionApproval(proposalId, transitionAuthority));
|
|
assertEq(governance.proposalTransitionJurisdictionApprovalCount(proposalId), 1);
|
|
assertEq(governance.proposalLastTransitionJurisdictionApprover(proposalId), transitionAuthority);
|
|
assertGe(governance.getProposalEta(proposalId), beforeQueue + 21 days);
|
|
|
|
vm.warp(governance.getProposalEta(proposalId));
|
|
governance.execute(proposalId);
|
|
|
|
UniversalAssetRegistry.UniversalAsset memory asset = registry.getAsset(address(token));
|
|
assertEq(token.primaryJurisdiction(), "SG");
|
|
assertEq(asset.jurisdiction, "SG");
|
|
assertEq(registry.getAssetJurisdictionId(address(token)), registry.jurisdictionIdFor("SG"));
|
|
}
|
|
}
|