// Published copy. Compile from a folder containing /contracts as served, with forge-std remapped. // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "forge-std/Test.sol"; import "../HolyGrailStablecoin.sol"; import "../InstitutionalSettlementDollar.sol"; import "../TransparentUpgradeableProxy.sol"; contract MockReserveOracle { bool public ok = true; uint256 public cap = 1_000_000e6; function set(bool _ok, uint256 _cap) external { ok = _ok; cap = _cap; } function verifyMintFeasibility(uint256, uint256) external view returns (bool, uint256) { return (ok, cap); } } /// HolyGrail USD: every role must be replaceable, and ownership must transfer in two steps. contract HolyGrailRolesTest is Test { HolyGrailUSD t; address owner = address(0xA11CE); address mm = address(0xB0B); address bl = address(0xB1AC); address pauser = address(0xD0); address rescuer = address(0xE0); address mallory = address(0xBAD); address fresh = address(0xF4E5); function setUp() public { HolyGrailUSD logic = new HolyGrailUSD(); bytes memory init = abi.encodeCall(HolyGrailUSD.initialize, ("HolyGrail USD", "HGUSD", 6, owner, mm, bl, pauser, rescuer)); t = HolyGrailUSD(address(new TransparentUpgradeableProxy(address(logic), address(0xAD), init))); } function test_implementationCannotBeInitializedDirectly() public { HolyGrailUSD logic = new HolyGrailUSD(); vm.prank(mallory); vm.expectRevert(bytes("Initializable: contract is already initialized")); logic.initialize("X", "X", 6, mallory, fresh, mallory, mallory, mallory); } function test_noKeyCanGrantItselfAQuota() public { vm.prank(mm); vm.expectRevert(bytes("Minter must be a separate key")); t.configureMinter(mm, 1e30); vm.prank(mm); vm.expectRevert(bytes("Minter must be a separate key")); t.configureMinter(owner, 1e30); vm.prank(owner); vm.expectRevert(bytes("Caller is not master minter")); t.configureMinter(owner, 1e30); vm.prank(owner); vm.expectRevert(bytes("Master minter must differ from owner")); t.proposeMasterMinter(owner); } /// R2-C: the owner key alone cannot install a new master minter without 48 hours' notice. function test_reviewR2C_masterMinterChangeIsTimelocked() public { vm.prank(owner); t.proposeMasterMinter(fresh); vm.prank(owner); vm.expectRevert(bytes("Master minter timelock not expired")); t.executeMasterMinterChange(); vm.warp(block.timestamp + t.ROLE_TIMELOCK()); vm.prank(owner); t.executeMasterMinterChange(); assertEq(t.masterMinter(), fresh); } /// R2 minor: a minter cannot become owner or master minter. function test_reviewR2M_minterCannotBecomeOwnerOrMasterMinter() public { address y = address(0x111); vm.prank(mm); t.configureMinter(y, 1e6); vm.prank(owner); t.transferOwnership(y); vm.prank(y); vm.expectRevert(bytes("Owner cannot be a minter")); t.acceptOwnership(); vm.prank(owner); vm.expectRevert(bytes("Master minter cannot be a minter")); t.proposeMasterMinter(y); } function test_compromisedBlacklisterCanBeReplaced() public { vm.prank(owner); t.setBlacklister(fresh); vm.prank(bl); // old (compromised) key no longer works vm.expectRevert(bytes("Caller is not blacklister")); t.blacklist(mallory); vm.prank(fresh); t.blacklist(mallory); assertTrue(t.isBlacklisted(mallory)); } function test_allRolesRotatableOnlyByOwner() public { vm.startPrank(mallory); vm.expectRevert(bytes("Ownable: caller is not the owner")); t.proposeMasterMinter(mallory); vm.expectRevert(bytes("Ownable: caller is not the owner")); t.setPauser(mallory); vm.expectRevert(bytes("Ownable: caller is not the owner")); t.setRescuer(mallory); vm.expectRevert(bytes("Ownable: caller is not the owner")); t.setBlacklister(mallory); vm.stopPrank(); vm.startPrank(owner); t.proposeMasterMinter(fresh); vm.warp(block.timestamp + t.ROLE_TIMELOCK()); t.executeMasterMinterChange(); t.setPauser(fresh); t.setRescuer(fresh); vm.stopPrank(); assertEq(t.masterMinter(), fresh); assertEq(t.pauser(), fresh); assertEq(t.rescuer(), fresh); } function test_rolesRejectZeroAddress() public { vm.prank(owner); vm.expectRevert(bytes("Zero address")); t.setBlacklister(address(0)); } function test_twoStepOwnership() public { vm.prank(owner); t.transferOwnership(fresh); assertEq(t.pendingOwner(), fresh); assertEq(t.owner(), owner); vm.prank(mallory); vm.expectRevert(bytes("Caller is not pending owner")); t.acceptOwnership(); vm.prank(fresh); t.acceptOwnership(); assertEq(t.owner(), fresh); assertEq(t.pendingOwner(), address(0)); } function test_dtiConstantIsHonestPlaceholder() public view { assertEq(t.DTI_IDENTIFIER(), bytes32("NOT-REGISTERED")); } } /// InstitutionalSettlementDollar: the owner must not bypass minter quotas. contract InstitutionalSettlementDollarQuotaTest is Test { InstitutionalSettlementDollar t; MockReserveOracle oracle; address owner = address(0xA11CE); address mm = address(0xB0B); address minter = address(0x111); function setUp() public { oracle = new MockReserveOracle(); t = new InstitutionalSettlementDollar(owner, mm, address(0xC0), address(0xD0), address(oracle)); vm.prank(mm); t.configureMinter(minter, 100e6); } /// Review I-2c: the owner must not be able to grant itself a quota and mint to the reserve cap. function test_reviewI2c_ownerCannotSelfAssignQuota() public { vm.prank(owner); vm.expectRevert(bytes("Minter: unauthorized")); t.configureMinter(owner, 10_000_000e6); vm.prank(mm); vm.expectRevert(bytes("Minter must be a separate key")); t.configureMinter(mm, 10_000_000e6); vm.prank(mm); vm.expectRevert(bytes("Minter must be a separate key")); t.configureMinter(owner, 10_000_000e6); assertEq(t.balanceOf(owner), 0); } function test_ownerCannotMintOutsideQuota() public { vm.prank(owner); vm.expectRevert(bytes("Unauthorized minter")); t.mint(owner, 1e6); } function test_masterMinterCannotMintWithoutOwnQuota() public { vm.prank(mm); vm.expectRevert(bytes("Unauthorized minter")); t.mint(mm, 1e6); } function test_minterWithinQuotaMintsAndQuotaDecrements() public { vm.prank(minter); t.mint(address(0xCAFE), 60e6); assertEq(t.minterAllowance(minter), 40e6); vm.prank(minter); vm.expectRevert(bytes("Minter allowance exceeded")); t.mint(address(0xCAFE), 41e6); } /// R2-C (ISD): the owner key alone cannot install a new master minter without 48 hours' notice. function test_reviewR2C_isdMasterMinterChangeIsTimelocked() public { address m2 = address(0x3E2); vm.prank(owner); t.proposeMasterMinter(m2); vm.prank(owner); vm.expectRevert(bytes("Master minter timelock not expired")); t.executeMasterMinterChange(); vm.warp(block.timestamp + t.ROLE_TIMELOCK()); vm.prank(owner); t.executeMasterMinterChange(); assertEq(t.masterMinter(), m2); } /// R2 minor (ISD): a minter cannot become owner or master minter. function test_reviewR2M_isdMinterCannotBecomeOwnerOrMasterMinter() public { vm.prank(owner); t.transferOwnership(minter); vm.prank(minter); vm.expectRevert(bytes("Owner cannot be a minter")); t.acceptOwnership(); vm.prank(owner); vm.expectRevert(bytes("Master minter cannot be a minter")); t.proposeMasterMinter(minter); } function test_reserveGateStillAppliesToQuotaMinter() public { oracle.set(false, 0); vm.prank(minter); vm.expectRevert(bytes("MINT REJECTED: Oracle reports reserve deficit, stale data (>24h), or risk buffer breach")); t.mint(address(0xCAFE), 1e6); } }