From 9b82fa1faa1c58ee52d49391b94f1c368e6bb587 Mon Sep 17 00:00:00 2001 From: Tronky Date: Wed, 5 Mar 2025 23:08:09 +0700 Subject: [PATCH 01/13] switched back to main branch for hamza-escrow submodule --- .gitmodules | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index a782c5b..44b846c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,4 +28,3 @@ [submodule "lib/hamza-escrow"] path = lib/hamza-escrow url = https://github.com/LoadPipe/hamza-escrow - branch = HAMT-37-security-context-refactor From f6dd163033b93ee7bdcaac487d391772325eb910 Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 09:58:00 +0700 Subject: [PATCH 02/13] added security context to GovernanceToken --- scripts/DeployHamzaVault.s.sol | 8 +++++++- src/tokens/GovernanceToken.sol | 8 +++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/DeployHamzaVault.s.sol b/scripts/DeployHamzaVault.s.sol index 56a2343..0139853 100644 --- a/scripts/DeployHamzaVault.s.sol +++ b/scripts/DeployHamzaVault.s.sol @@ -119,7 +119,11 @@ contract DeployHamzaVault is Script { (address newBaalAddr, address payable vault, address lootTokenAddr) = deployBaalAndCommunityVault(); // 9-10) Deploy governance token and vault - (address govTokenAddr, address govVaultAddr) = deployGovernanceContracts(lootTokenAddr, vault); + (address govTokenAddr, address govVaultAddr) = deployGovernanceContracts( + ISecurityContext(hatsSecurityContextAddr), + lootTokenAddr, + vault + ); // 11-14) Deploy Timelock and Governor address timelockAddr = deployGovernorAndTimelock(govTokenAddr); @@ -305,6 +309,7 @@ contract DeployHamzaVault is Script { } function deployGovernanceContracts( + ISecurityContext securityContext, address lootTokenAddr, address payable vault ) internal returns ( @@ -316,6 +321,7 @@ contract DeployHamzaVault is Script { string memory govTokenSymbol = config.readString(".governanceToken.symbol"); GovernanceToken govToken = new GovernanceToken( + securityContext, IERC20(lootTokenAddr), govTokenName, govTokenSymbol diff --git a/src/tokens/GovernanceToken.sol b/src/tokens/GovernanceToken.sol index 0c6b157..0c9fed4 100644 --- a/src/tokens/GovernanceToken.sol +++ b/src/tokens/GovernanceToken.sol @@ -8,9 +8,11 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; import "@hamza-escrow/security/HasSecurityContext.sol"; -contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { - constructor(IERC20 wrappedToken, string memory name_, string memory symbol_) - ERC20("HamGov", "HAM") ERC20Permit("HamGov") ERC20Wrapper(wrappedToken) {} +contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper, HasSecurityContext { + constructor(ISecurityContext _securityContext, IERC20 wrappedToken, string memory name_, string memory symbol_) + ERC20("HamGov", "HAM") ERC20Permit("HamGov") ERC20Wrapper(wrappedToken) { + _setSecurityContext(_securityContext); + } function decimals() public view override(ERC20, ERC20Wrapper) returns(uint8) { return 18; From ae886d96bcd66063605ccb929a40161901cc0840 Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 10:31:20 +0700 Subject: [PATCH 03/13] added minter hat --- scripts/DeployHamzaVault.s.sol | 27 +++++++++++++-- scripts/HatsDeployment.s.sol | 62 ++++++++++++++++++++++++++++++++-- src/tokens/GovernanceToken.sol | 2 +- test/Voting.t.sol | 6 ++-- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/scripts/DeployHamzaVault.s.sol b/scripts/DeployHamzaVault.s.sol index 0139853..e928dba 100644 --- a/scripts/DeployHamzaVault.s.sol +++ b/scripts/DeployHamzaVault.s.sol @@ -62,6 +62,7 @@ contract DeployHamzaVault is Script { address public hats; address public hatsSecurityContextAddr; uint256 public daoHatId; + uint256 public minterHatId; string public config; function run() @@ -102,6 +103,7 @@ contract DeployHamzaVault is Script { address _hats, uint256 _adminHatId, uint256 _daoHatId, + uint256 _minterHatId, address _hatsSecurityContextAddr ) = deployHats(); @@ -110,6 +112,7 @@ contract DeployHamzaVault is Script { hats = _hats; adminHatId = _adminHatId; daoHatId = _daoHatId; + minterHatId = _minterHatId; hatsSecurityContextAddr = _hatsSecurityContextAddr; // 4) Start broadcast for subsequent deployments @@ -159,6 +162,7 @@ contract DeployHamzaVault is Script { address _hats, uint256 _adminHatId, uint256 _daoHatId, + uint256 _minterHatId, address _hatsSecurityContextAddr ) { HatsDeployment hatsDeployment = new HatsDeployment(); @@ -179,7 +183,8 @@ contract DeployHamzaVault is Script { arbiterHatId, _daoHatId, systemHatId, - pauserHatId + pauserHatId, + _minterHatId ) = hatsDeployment.run(); return ( @@ -187,6 +192,7 @@ contract DeployHamzaVault is Script { _hats, _adminHatId, _daoHatId, + _minterHatId, _hatsSecurityContextAddr ); } @@ -341,8 +347,25 @@ contract DeployHamzaVault is Script { govVaultAddr = address(govVault); // link the community vault <-> governance vault - CommunityVault(vault).setGovernanceVault(address(govVault), lootTokenAddr); + CommunityVault(vault).setGovernanceVault(govVaultAddr, lootTokenAddr); govVault.setCommunityVault(vault); + + //Assign minter hat to the governance vault + { + bytes memory data = abi.encodeWithSelector( + Hats.mintHat.selector, + minterHatId, + govVaultAddr + ); + + SafeTransactionHelper.execTransaction( + safeAddr, + hats, + 0, + data, + OWNER_ONE + ); + } return (govTokenAddr, govVaultAddr); } diff --git a/scripts/HatsDeployment.s.sol b/scripts/HatsDeployment.s.sol index 79cd452..ccf14fc 100644 --- a/scripts/HatsDeployment.s.sol +++ b/scripts/HatsDeployment.s.sol @@ -47,6 +47,7 @@ contract HatsDeployment is Script { uint256 public topHatId; uint256 public systemHatId; uint256 public pauserHatId; + uint256 public minterHatId; // Deployer's private key and derived addresses uint256 internal deployerPk; @@ -79,7 +80,8 @@ contract HatsDeployment is Script { uint256 _arbiterHatId, uint256 _daoHatId, uint256 _systemHatId, - uint256 _pauserHatId + uint256 _pauserHatId, + uint256 _minterHatId ) { @@ -212,6 +214,22 @@ contract HatsDeployment is Script { execTransaction(safeAddr, address(hats), 0, data); } + // Minter + { + minterHatId = hats.getNextId(adminHatId); + bytes memory data = abi.encodeWithSelector( + Hats.createHat.selector, + adminHatId, + "ipfs://bafkreiczfbtftesggzcfnumcy7rfru665a77uyznbabdk5b6ftfo2hvjw1", + uint32(100), + address(eligibilityModule), + address(toggleModule), + true, + "" + ); + execTransaction(safeAddr, address(hats), 0, data); + } + // 8) Deploy HatsSecurityContext & set role hats securityContext = new HatsSecurityContext(address(hats), adminHatId); @@ -257,6 +275,14 @@ contract HatsDeployment is Script { true ); execTransaction(safeAddr, address(eligibilityModule), 0, data); + + data = abi.encodeWithSelector( + EligibilityModule.setHatRules.selector, + minterHatId, + true, + true + ); + execTransaction(safeAddr, address(eligibilityModule), 0, data); } { // Toggle hats "active" in the toggle module @@ -294,6 +320,13 @@ contract HatsDeployment is Script { true ); execTransaction(safeAddr, address(toggleModule), 0, data); + + data = abi.encodeWithSelector( + ToggleModule.setHatStatus.selector, + minterHatId, + true + ); + execTransaction(safeAddr, address(toggleModule), 0, data); } // 10) Mint hats to relevant addresses @@ -354,6 +387,22 @@ contract HatsDeployment is Script { ); execTransaction(safeAddr, address(hats), 0, data); } + // Minter + { + bytes memory data = abi.encodeWithSelector( + Hats.mintHat.selector, + minterHatId, + adminAddress1 + ); + execTransaction(safeAddr, address(hats), 0, data); + + data = abi.encodeWithSelector( + Hats.mintHat.selector, + minterHatId, + adminAddress2 + ); + execTransaction(safeAddr, address(hats), 0, data); + } // 11) Set role hats in the HatsSecurityContext { @@ -384,6 +433,13 @@ contract HatsDeployment is Script { pauserHatId ); execTransaction(safeAddr, address(securityContext), 0, data); + + data = abi.encodeWithSelector( + HatsSecurityContext.setRoleHat.selector, + Roles.MINTER_ROLE, + minterHatId + ); + execTransaction(safeAddr, address(securityContext), 0, data); } vm.stopBroadcast(); @@ -396,6 +452,7 @@ contract HatsDeployment is Script { console2.log("DAO Hat ID: ", daoHatId); console2.log("System Hat ID: ", systemHatId); console2.log("Pauser Hat ID: ", pauserHatId); + console2.log("Minter Hat ID: ", minterHatId); console2.log("-----------------------------------------------"); console2.log("Hats Address is: ", address(hats)); @@ -417,7 +474,8 @@ contract HatsDeployment is Script { arbiterHatId, daoHatId, systemHatId, - pauserHatId + pauserHatId, + minterHatId ); } } diff --git a/src/tokens/GovernanceToken.sol b/src/tokens/GovernanceToken.sol index 0c9fed4..bb8914a 100644 --- a/src/tokens/GovernanceToken.sol +++ b/src/tokens/GovernanceToken.sol @@ -9,7 +9,7 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; import "@hamza-escrow/security/HasSecurityContext.sol"; contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper, HasSecurityContext { - constructor(ISecurityContext _securityContext, IERC20 wrappedToken, string memory name_, string memory symbol_) + constructor(ISecurityContext _securityContext, IERC20 wrappedToken, string memory /*name_*/, string memory /*symbol_*/) ERC20("HamGov", "HAM") ERC20Permit("HamGov") ERC20Wrapper(wrappedToken) { _setSecurityContext(_securityContext); } diff --git a/test/Voting.t.sol b/test/Voting.t.sol index 88d21c7..75ac756 100644 --- a/test/Voting.t.sol +++ b/test/Voting.t.sol @@ -53,9 +53,12 @@ contract VotingTest is Test { voters.push(address(0x20)); voters.push(address(0x21)); + //security context + securityContext = createHatsSecurityContext(); + //create tokens & mint lootToken = new TestToken("LOOT", "LOOT"); - govToken = new GovernanceToken(lootToken, "Hamg", "HAMG"); + govToken = new GovernanceToken(securityContext, lootToken, "Hamg", "HAMG"); //mint tokens for(uint256 n=0; n Date: Thu, 6 Mar 2025 12:01:27 +0700 Subject: [PATCH 04/13] reorganized hats wrangling logic --- scripts/HatsDeployment.s.sol | 369 +++++++++-------------------------- 1 file changed, 90 insertions(+), 279 deletions(-) diff --git a/scripts/HatsDeployment.s.sol b/scripts/HatsDeployment.s.sol index ccf14fc..e6e86d7 100644 --- a/scripts/HatsDeployment.s.sol +++ b/scripts/HatsDeployment.s.sol @@ -149,298 +149,56 @@ contract HatsDeployment is Script { adminHatId = uint256(hats.lastTopHatId()) << 224; // 7) Create child Hats - - // Arbiter - { - arbiterHatId = hats.getNextId(adminHatId); - bytes memory data = abi.encodeWithSelector( - Hats.createHat.selector, - adminHatId, - "ipfs://bafkreicbhbvddt2f475inukntzh6n72ehm4iyljstyyjsmizdsojmbdase", - uint32(2), - address(eligibilityModule), - address(toggleModule), - true, - "" - ); - execTransaction(safeAddr, address(hats), 0, data); - } - - // DAO - { - daoHatId = hats.getNextId(adminHatId); - bytes memory data = abi.encodeWithSelector( - Hats.createHat.selector, - adminHatId, - "ipfs://bafkreic2f5b6ykdvafs5nhkouruvlql73caou5etgdrx67yt6ofp6pwf24", - uint32(2), - address(eligibilityModule), - address(toggleModule), - true, - "" - ); - execTransaction(safeAddr, address(hats), 0, data); - } - - // System - { - systemHatId = hats.getNextId(adminHatId); - bytes memory data = abi.encodeWithSelector( - Hats.createHat.selector, - adminHatId, - "ipfs://bafkreie2vxohaw7cneknlwv6hq7h4askkv6jfcadho6efz5bxfx66fqu3q", - uint32(2), - address(eligibilityModule), - address(toggleModule), - true, - "" - ); - execTransaction(safeAddr, address(hats), 0, data); - } - - // Pauser - { - pauserHatId = hats.getNextId(adminHatId); - bytes memory data = abi.encodeWithSelector( - Hats.createHat.selector, - adminHatId, - "ipfs://bafkreiczfbtftesggzcfnumcy7rfru665a77uyznbabdk5b6ftfo2hvjw4", - uint32(2), - address(eligibilityModule), - address(toggleModule), - true, - "" - ); - execTransaction(safeAddr, address(hats), 0, data); - } - - // Minter - { - minterHatId = hats.getNextId(adminHatId); - bytes memory data = abi.encodeWithSelector( - Hats.createHat.selector, - adminHatId, - "ipfs://bafkreiczfbtftesggzcfnumcy7rfru665a77uyznbabdk5b6ftfo2hvjw1", - uint32(100), - address(eligibilityModule), - address(toggleModule), - true, - "" - ); - execTransaction(safeAddr, address(hats), 0, data); - } + arbiterHatId = createHat("ipfs://bafkreicbhbvddt2f475inukntzh6n72ehm4iyljstyyjsmizdsojmbdase", 2); + daoHatId = createHat("ipfs://bafkreic2f5b6ykdvafs5nhkouruvlql73caou5etgdrx67yt6ofp6pwf24", 2); + systemHatId = createHat("ipfs://bafkreie2vxohaw7cneknlwv6hq7h4askkv6jfcadho6efz5bxfx66fqu3q", 2); + pauserHatId = createHat("ipfs://bafkreiczfbtftesggzcfnumcy7rfru665a77uyznbabdk5b6ftfo2hvjw4", 2); + minterHatId = createHat("ipfs://bafkreiczfbtftesggzcfnumcy7rfru665a77uyznbabdk5b6ftfo2hvjw1", 100); // 8) Deploy HatsSecurityContext & set role hats securityContext = new HatsSecurityContext(address(hats), adminHatId); // 9) Configure eligibility + toggle modules - { - // Configure each hat's rules in the eligibility module - bytes memory data = abi.encodeWithSelector( - EligibilityModule.setHatRules.selector, - adminHatId, - true, - true - ); - execTransaction(safeAddr, address(eligibilityModule), 0, data); - - data = abi.encodeWithSelector( - EligibilityModule.setHatRules.selector, - arbiterHatId, - true, - true - ); - execTransaction(safeAddr, address(eligibilityModule), 0, data); - - data = abi.encodeWithSelector( - EligibilityModule.setHatRules.selector, - daoHatId, - true, - true - ); - execTransaction(safeAddr, address(eligibilityModule), 0, data); - - data = abi.encodeWithSelector( - EligibilityModule.setHatRules.selector, - systemHatId, - true, - true - ); - execTransaction(safeAddr, address(eligibilityModule), 0, data); - - data = abi.encodeWithSelector( - EligibilityModule.setHatRules.selector, - pauserHatId, - true, - true - ); - execTransaction(safeAddr, address(eligibilityModule), 0, data); - - data = abi.encodeWithSelector( - EligibilityModule.setHatRules.selector, - minterHatId, - true, - true - ); - execTransaction(safeAddr, address(eligibilityModule), 0, data); - } - { - // Toggle hats "active" in the toggle module - bytes memory data = abi.encodeWithSelector( - ToggleModule.setHatStatus.selector, - adminHatId, - true - ); - execTransaction(safeAddr, address(toggleModule), 0, data); - - data = abi.encodeWithSelector( - ToggleModule.setHatStatus.selector, - arbiterHatId, - true - ); - execTransaction(safeAddr, address(toggleModule), 0, data); - - data = abi.encodeWithSelector( - ToggleModule.setHatStatus.selector, - daoHatId, - true - ); - execTransaction(safeAddr, address(toggleModule), 0, data); - - data = abi.encodeWithSelector( - ToggleModule.setHatStatus.selector, - systemHatId, - true - ); - execTransaction(safeAddr, address(toggleModule), 0, data); - - data = abi.encodeWithSelector( - ToggleModule.setHatStatus.selector, - pauserHatId, - true - ); - execTransaction(safeAddr, address(toggleModule), 0, data); - - data = abi.encodeWithSelector( - ToggleModule.setHatStatus.selector, - minterHatId, - true - ); - execTransaction(safeAddr, address(toggleModule), 0, data); - } + setHatRules(adminHatId, true, true); + setHatRules(arbiterHatId, true, true); + setHatRules(daoHatId, true, true); + setHatRules(systemHatId, true, true); + setHatRules(pauserHatId, true, true); + setHatRules(minterHatId, true, true); + + setHatStatus(adminHatId, true); + setHatStatus(arbiterHatId, true); + setHatStatus(daoHatId, true); + setHatStatus(systemHatId, true); + setHatStatus(pauserHatId, true); + setHatStatus(minterHatId, true); // 10) Mint hats to relevant addresses - // Arbiter - { - bytes memory data = abi.encodeWithSelector( - Hats.mintHat.selector, - arbiterHatId, - adminAddress1 - ); - execTransaction(safeAddr, address(hats), 0, data); + //Arbiter + mintHat(arbiterHatId, adminAddress1); + mintHat(arbiterHatId, adminAddress2); - data = abi.encodeWithSelector( - Hats.mintHat.selector, - arbiterHatId, - adminAddress2 - ); - execTransaction(safeAddr, address(hats), 0, data); - } - // DAO - { - bytes memory data = abi.encodeWithSelector( - Hats.mintHat.selector, - daoHatId, - adminAddress1 - ); - execTransaction(safeAddr, address(hats), 0, data); - } - // System - { - bytes memory data = abi.encodeWithSelector( - Hats.mintHat.selector, - systemHatId, - adminAddress1 - ); - execTransaction(safeAddr, address(hats), 0, data); + //DAO + mintHat(daoHatId, adminAddress1); - data = abi.encodeWithSelector( - Hats.mintHat.selector, - systemHatId, - adminAddress2 - ); - execTransaction(safeAddr, address(hats), 0, data); - } - // Pauser - { - bytes memory data = abi.encodeWithSelector( - Hats.mintHat.selector, - pauserHatId, - adminAddress1 - ); - execTransaction(safeAddr, address(hats), 0, data); + //System + mintHat(systemHatId, adminAddress1); + mintHat(systemHatId, adminAddress2); - data = abi.encodeWithSelector( - Hats.mintHat.selector, - pauserHatId, - adminAddress2 - ); - execTransaction(safeAddr, address(hats), 0, data); - } - // Minter - { - bytes memory data = abi.encodeWithSelector( - Hats.mintHat.selector, - minterHatId, - adminAddress1 - ); - execTransaction(safeAddr, address(hats), 0, data); + //Pauser + mintHat(pauserHatId, adminAddress1); + mintHat(pauserHatId, adminAddress2); - data = abi.encodeWithSelector( - Hats.mintHat.selector, - minterHatId, - adminAddress2 - ); - execTransaction(safeAddr, address(hats), 0, data); - } + //Minter + mintHat(minterHatId, adminAddress1); + mintHat(minterHatId, adminAddress2); // 11) Set role hats in the HatsSecurityContext - { - bytes memory data = abi.encodeWithSelector( - HatsSecurityContext.setRoleHat.selector, - Roles.ARBITER_ROLE, - arbiterHatId - ); - execTransaction(safeAddr, address(securityContext), 0, data); - - data = abi.encodeWithSelector( - HatsSecurityContext.setRoleHat.selector, - Roles.DAO_ROLE, - daoHatId - ); - execTransaction(safeAddr, address(securityContext), 0, data); - - data = abi.encodeWithSelector( - HatsSecurityContext.setRoleHat.selector, - Roles.SYSTEM_ROLE, - systemHatId - ); - execTransaction(safeAddr, address(securityContext), 0, data); - - data = abi.encodeWithSelector( - HatsSecurityContext.setRoleHat.selector, - Roles.PAUSER_ROLE, - pauserHatId - ); - execTransaction(safeAddr, address(securityContext), 0, data); - - data = abi.encodeWithSelector( - HatsSecurityContext.setRoleHat.selector, - Roles.MINTER_ROLE, - minterHatId - ); - execTransaction(safeAddr, address(securityContext), 0, data); - } + setHatRole(arbiterHatId, Roles.ARBITER_ROLE); + setHatRole(daoHatId, Roles.DAO_ROLE); + setHatRole(systemHatId, Roles.SYSTEM_ROLE); + setHatRole(pauserHatId, Roles.PAUSER_ROLE); + setHatRole(minterHatId, Roles.MINTER_ROLE); vm.stopBroadcast(); @@ -478,4 +236,57 @@ contract HatsDeployment is Script { minterHatId ); } + + function createHat(string memory uri, uint32 maxSupply) private returns (uint256) { + uint256 hatId = hats.getNextId(adminHatId); + bytes memory data = abi.encodeWithSelector( + Hats.createHat.selector, + adminHatId, + uri, + maxSupply, + address(eligibilityModule), + address(toggleModule), + true, + "" + ); + execTransaction(deployedSafe, address(hats), 0, data); + return hatId; + } + + function setHatRules(uint256 hatId, bool eligible, bool standing) private { + bytes memory data = abi.encodeWithSelector( + EligibilityModule.setHatRules.selector, + hatId, + eligible, + standing + ); + execTransaction(deployedSafe, address(eligibilityModule), 0, data); + } + + function setHatStatus(uint256 hatId, bool active) private { + bytes memory data = abi.encodeWithSelector( + ToggleModule.setHatStatus.selector, + hatId, + active + ); + execTransaction(deployedSafe, address(toggleModule), 0, data); + } + + function mintHat(uint256 hatId, address recipient) private { + bytes memory data = abi.encodeWithSelector( + Hats.mintHat.selector, + hatId, + recipient + ); + execTransaction(deployedSafe, address(hats), 0, data); + } + + function setHatRole(uint256 hatId, bytes32 role) private { + bytes memory data = abi.encodeWithSelector( + HatsSecurityContext.setRoleHat.selector, + role, + hatId + ); + execTransaction(deployedSafe, address(securityContext), 0, data); + } } From a12d4100259dd0b62f42eb9e63a281ae121b57f6 Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 12:01:48 +0700 Subject: [PATCH 05/13] removed compile warnings --- src/tokens/GovernanceToken.sol | 2 +- test/FullRound.t.sol | 3 +-- test/GovernanceVault.t.sol | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tokens/GovernanceToken.sol b/src/tokens/GovernanceToken.sol index bb8914a..7e672cc 100644 --- a/src/tokens/GovernanceToken.sol +++ b/src/tokens/GovernanceToken.sol @@ -14,7 +14,7 @@ contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper, HasSec _setSecurityContext(_securityContext); } - function decimals() public view override(ERC20, ERC20Wrapper) returns(uint8) { + function decimals() public pure override(ERC20, ERC20Wrapper) returns(uint8) { return 18; } diff --git a/test/FullRound.t.sol b/test/FullRound.t.sol index 03f0a72..bda28e5 100644 --- a/test/FullRound.t.sol +++ b/test/FullRound.t.sol @@ -18,7 +18,7 @@ import "./DeploymentSetup.t.sol"; contract FullRound is DeploymentSetup { // Basic deployment checks - function testDeployment() public { + function testDeployment() public view { assertTrue(baal != address(0), "Baal address is zero"); assertTrue(communityVault != address(0), "CommunityVault address is zero"); assertTrue(govToken != address(0), "GovernanceToken address is zero"); @@ -58,7 +58,6 @@ contract FullRound is DeploymentSetup { // End-to-End Flow: deposit + vest + distribute rewards function testVestingAndRewardsFlow() public { // Setup references - CommunityVault cVault = CommunityVault(communityVault); GovernanceVault gVault = GovernanceVault(govVault); GovernanceToken gToken = GovernanceToken(govToken); IERC20 lToken = IERC20(lootToken); diff --git a/test/GovernanceVault.t.sol b/test/GovernanceVault.t.sol index dc92a33..f21e20d 100644 --- a/test/GovernanceVault.t.sol +++ b/test/GovernanceVault.t.sol @@ -28,7 +28,7 @@ contract GovernanceVaultTest is DeploymentSetup { } // Test that the vault is initialized as expected - function testGovernanceVaultSetup() public { + function testGovernanceVaultSetup() public view { // Check vault addresses are not zero assertTrue(address(gVault) != address(0), "governanceVault is zero address"); assertTrue(address(gToken) != address(0), "governanceToken is zero address"); From 7492c337bbd817913337aa7385ebf6026a428866 Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 12:12:41 +0700 Subject: [PATCH 06/13] added burner role --- scripts/DeployHamzaVault.s.sol | 60 ++++++++++++++++------------------ scripts/HatsDeployment.s.sol | 16 +++++++-- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/scripts/DeployHamzaVault.s.sol b/scripts/DeployHamzaVault.s.sol index e928dba..5f36f52 100644 --- a/scripts/DeployHamzaVault.s.sol +++ b/scripts/DeployHamzaVault.s.sol @@ -63,6 +63,7 @@ contract DeployHamzaVault is Script { address public hatsSecurityContextAddr; uint256 public daoHatId; uint256 public minterHatId; + uint256 public burnerHatId; string public config; function run() @@ -104,6 +105,7 @@ contract DeployHamzaVault is Script { uint256 _adminHatId, uint256 _daoHatId, uint256 _minterHatId, + uint256 _burnerHatId, address _hatsSecurityContextAddr ) = deployHats(); @@ -113,6 +115,7 @@ contract DeployHamzaVault is Script { adminHatId = _adminHatId; daoHatId = _daoHatId; minterHatId = _minterHatId; + burnerHatId = _burnerHatId; hatsSecurityContextAddr = _hatsSecurityContextAddr; // 4) Start broadcast for subsequent deployments @@ -163,6 +166,7 @@ contract DeployHamzaVault is Script { uint256 _adminHatId, uint256 _daoHatId, uint256 _minterHatId, + uint256 _burnerHatId, address _hatsSecurityContextAddr ) { HatsDeployment hatsDeployment = new HatsDeployment(); @@ -184,7 +188,8 @@ contract DeployHamzaVault is Script { _daoHatId, systemHatId, pauserHatId, - _minterHatId + _minterHatId, + _burnerHatId ) = hatsDeployment.run(); return ( @@ -193,6 +198,7 @@ contract DeployHamzaVault is Script { _adminHatId, _daoHatId, _minterHatId, + _burnerHatId, _hatsSecurityContextAddr ); } @@ -350,22 +356,9 @@ contract DeployHamzaVault is Script { CommunityVault(vault).setGovernanceVault(govVaultAddr, lootTokenAddr); govVault.setCommunityVault(vault); - //Assign minter hat to the governance vault - { - bytes memory data = abi.encodeWithSelector( - Hats.mintHat.selector, - minterHatId, - govVaultAddr - ); - - SafeTransactionHelper.execTransaction( - safeAddr, - hats, - 0, - data, - OWNER_ONE - ); - } + //Assign minter & burner hats to the governance vault + assignHat(minterHatId, govVaultAddr); + assignHat(burnerHatId, govVaultAddr); return (govTokenAddr, govVaultAddr); } @@ -405,21 +398,7 @@ contract DeployHamzaVault is Script { timelock.grantRole(0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1, address(governor)); // 14) grant timelock dao role to the governor - { - bytes memory data = abi.encodeWithSelector( - Hats.mintHat.selector, - daoHatId, - address(timelock) - ); - - SafeTransactionHelper.execTransaction( - safeAddr, - hats, - 0, - data, - OWNER_ONE - ); - } + assignHat(daoHatId, address(timelock)); return timelockAddr; } @@ -495,4 +474,21 @@ contract DeployHamzaVault is Script { arr[0] = _val; return arr; } + + function assignHat(uint256 hatId, address recipient) private + { + bytes memory data = abi.encodeWithSelector( + Hats.mintHat.selector, + hatId, + recipient + ); + + SafeTransactionHelper.execTransaction( + safeAddr, + hats, + 0, + data, + OWNER_ONE + ); + } } diff --git a/scripts/HatsDeployment.s.sol b/scripts/HatsDeployment.s.sol index e6e86d7..7f7e251 100644 --- a/scripts/HatsDeployment.s.sol +++ b/scripts/HatsDeployment.s.sol @@ -48,6 +48,7 @@ contract HatsDeployment is Script { uint256 public systemHatId; uint256 public pauserHatId; uint256 public minterHatId; + uint256 public burnerHatId; // Deployer's private key and derived addresses uint256 internal deployerPk; @@ -81,7 +82,8 @@ contract HatsDeployment is Script { uint256 _daoHatId, uint256 _systemHatId, uint256 _pauserHatId, - uint256 _minterHatId + uint256 _minterHatId, + uint256 _burnerHatId ) { @@ -154,6 +156,7 @@ contract HatsDeployment is Script { systemHatId = createHat("ipfs://bafkreie2vxohaw7cneknlwv6hq7h4askkv6jfcadho6efz5bxfx66fqu3q", 2); pauserHatId = createHat("ipfs://bafkreiczfbtftesggzcfnumcy7rfru665a77uyznbabdk5b6ftfo2hvjw4", 2); minterHatId = createHat("ipfs://bafkreiczfbtftesggzcfnumcy7rfru665a77uyznbabdk5b6ftfo2hvjw1", 100); + burnerHatId = createHat("ipfs://bafkreiczfbtftesggzcfnumcy7rfru665a77uyznbabdk5b6ftfo2hvjw2", 100); // 8) Deploy HatsSecurityContext & set role hats securityContext = new HatsSecurityContext(address(hats), adminHatId); @@ -165,6 +168,7 @@ contract HatsDeployment is Script { setHatRules(systemHatId, true, true); setHatRules(pauserHatId, true, true); setHatRules(minterHatId, true, true); + setHatRules(burnerHatId, true, true); setHatStatus(adminHatId, true); setHatStatus(arbiterHatId, true); @@ -172,6 +176,7 @@ contract HatsDeployment is Script { setHatStatus(systemHatId, true); setHatStatus(pauserHatId, true); setHatStatus(minterHatId, true); + setHatStatus(burnerHatId, true); // 10) Mint hats to relevant addresses //Arbiter @@ -193,12 +198,17 @@ contract HatsDeployment is Script { mintHat(minterHatId, adminAddress1); mintHat(minterHatId, adminAddress2); + //Burner + mintHat(burnerHatId, adminAddress1); + mintHat(burnerHatId, adminAddress2); + // 11) Set role hats in the HatsSecurityContext setHatRole(arbiterHatId, Roles.ARBITER_ROLE); setHatRole(daoHatId, Roles.DAO_ROLE); setHatRole(systemHatId, Roles.SYSTEM_ROLE); setHatRole(pauserHatId, Roles.PAUSER_ROLE); setHatRole(minterHatId, Roles.MINTER_ROLE); + setHatRole(burnerHatId, Roles.BURNER_ROLE); vm.stopBroadcast(); @@ -211,6 +221,7 @@ contract HatsDeployment is Script { console2.log("System Hat ID: ", systemHatId); console2.log("Pauser Hat ID: ", pauserHatId); console2.log("Minter Hat ID: ", minterHatId); + console2.log("Burner Hat ID: ", burnerHatId); console2.log("-----------------------------------------------"); console2.log("Hats Address is: ", address(hats)); @@ -233,7 +244,8 @@ contract HatsDeployment is Script { daoHatId, systemHatId, pauserHatId, - minterHatId + minterHatId, + burnerHatId ); } From c8342b66c565dc73edd1477d83cb8da3def53ab9 Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 13:25:14 +0700 Subject: [PATCH 07/13] Voting tests modified to pass with security permissions --- src/tokens/GovernanceToken.sol | 8 +++---- test/Voting.t.sol | 39 ++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/tokens/GovernanceToken.sol b/src/tokens/GovernanceToken.sol index 7e672cc..9b2f694 100644 --- a/src/tokens/GovernanceToken.sol +++ b/src/tokens/GovernanceToken.sol @@ -18,19 +18,19 @@ contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper, HasSec return 18; } - function mint(address to, uint256 amount) external /* onlyRole(MINTER_ROLE) */ { + function mint(address to, uint256 amount) external onlyRole(Roles.MINTER_ROLE) { _mint(to, amount); } - function burn(address account, uint256 amount) external /* onlyRole(BURNER_ROLE) */ { + function burn(address account, uint256 amount) external onlyRole(Roles.BURNER_ROLE) { _burn(account, amount); } - function depositFor(address account, uint256 amount) public override(ERC20Wrapper) returns (bool) /* onlyRole(MINTER_ROLE) */ { + function depositFor(address account, uint256 amount) public override(ERC20Wrapper) onlyRole(Roles.MINTER_ROLE) returns (bool) { return super.depositFor(account, amount); } - function withdrawTo(address account, uint256 amount) public override(ERC20Wrapper) returns (bool) /* onlyRole(MINTER_ROLE) */ { + function withdrawTo(address account, uint256 amount) public override(ERC20Wrapper) onlyRole(Roles.MINTER_ROLE) returns (bool) { return super.withdrawTo(account, amount); } diff --git a/test/Voting.t.sol b/test/Voting.t.sol index 75ac756..2d556e4 100644 --- a/test/Voting.t.sol +++ b/test/Voting.t.sol @@ -65,14 +65,6 @@ contract VotingTest is Test { lootToken.mint(voters[n], 100); } - //wrap tokens for voters - for(uint8 n=0; n Date: Thu, 6 Mar 2025 13:47:31 +0700 Subject: [PATCH 08/13] SecurityContext for CommunityVault --- scripts/DeployHamzaVault.s.sol | 1 + src/GovernanceVault.sol | 16 +++++++++------- src/HamzaGovernor.sol | 8 -------- src/tokens/GovernanceToken.sol | 4 ++-- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/scripts/DeployHamzaVault.s.sol b/scripts/DeployHamzaVault.s.sol index 5f36f52..929e145 100644 --- a/scripts/DeployHamzaVault.s.sol +++ b/scripts/DeployHamzaVault.s.sol @@ -345,6 +345,7 @@ contract DeployHamzaVault is Script { uint256 vestingPeriod = config.readUint(".governanceVault.vestingPeriod"); GovernanceVault govVault = new GovernanceVault( + securityContext, lootTokenAddr, GovernanceToken(address(govToken)), vestingPeriod diff --git a/src/GovernanceVault.sol b/src/GovernanceVault.sol index f13b15e..83e683f 100644 --- a/src/GovernanceVault.sol +++ b/src/GovernanceVault.sol @@ -6,7 +6,6 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./tokens/GovernanceToken.sol"; import "@hamza-escrow/security/HasSecurityContext.sol"; -//TODO: does it need to have its SecurityContext set? contract GovernanceVault is HasSecurityContext { using SafeERC20 for IERC20; @@ -28,6 +27,7 @@ contract GovernanceVault is HasSecurityContext { event RewardDistributed(address indexed staker, uint256 amount); constructor( + ISecurityContext securityContext, address lootTokenAddress, GovernanceToken governanceTokenAddress, uint256 vestingPeriod @@ -35,6 +35,8 @@ contract GovernanceVault is HasSecurityContext { lootToken = IERC20(lootTokenAddress); governanceToken = governanceTokenAddress; vestingPeriodSeconds = vestingPeriod; + + _setSecurityContext(securityContext); } // deposit loot tokens into the vault @@ -94,6 +96,12 @@ contract GovernanceVault is HasSecurityContext { _processRewardDistribution(staker, totalReward); } + // admin function to set the community vault address + function setCommunityVault(address _communityVault) external { + require(_communityVault != address(0), "Invalid address"); + communityVault = _communityVault; + } + // helper function to process new deposits function _processDeposit(address account, uint256 amount) private { deposits[account].push(Deposit({ @@ -141,10 +149,4 @@ contract GovernanceVault is HasSecurityContext { governanceToken.mint(staker, amount); emit RewardDistributed(staker, amount); } - - // admin function to set the community vault address - function setCommunityVault(address _communityVault) external { - require(_communityVault != address(0), "Invalid address"); - communityVault = _communityVault; - } } \ No newline at end of file diff --git a/src/HamzaGovernor.sol b/src/HamzaGovernor.sol index 8d68521..f9e9c29 100644 --- a/src/HamzaGovernor.sol +++ b/src/HamzaGovernor.sol @@ -80,14 +80,6 @@ contract HamzaGovernor is return super._executor(); } - /*function _getVotes( - address account, - uint256 blockNumber, - bytes memory params - ) internal view override(Governor, GovernorVotes) returns (uint256) { - return token.getVotes(account, blockNumber, params); - }*/ - function supportsInterface( bytes4 interfaceId ) public view override(Governor, IERC165, GovernorTimelockControl) returns (bool) { diff --git a/src/tokens/GovernanceToken.sol b/src/tokens/GovernanceToken.sol index 9b2f694..8d6de23 100644 --- a/src/tokens/GovernanceToken.sol +++ b/src/tokens/GovernanceToken.sol @@ -9,9 +9,9 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; import "@hamza-escrow/security/HasSecurityContext.sol"; contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper, HasSecurityContext { - constructor(ISecurityContext _securityContext, IERC20 wrappedToken, string memory /*name_*/, string memory /*symbol_*/) + constructor(ISecurityContext securityContext, IERC20 wrappedToken, string memory /*name_*/, string memory /*symbol_*/) ERC20("HamGov", "HAM") ERC20Permit("HamGov") ERC20Wrapper(wrappedToken) { - _setSecurityContext(_securityContext); + _setSecurityContext(securityContext); } function decimals() public pure override(ERC20, ERC20Wrapper) returns(uint8) { From 08fe0322440eb35ed3b17e365a8c3baa5c2af166 Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 13:55:05 +0700 Subject: [PATCH 09/13] SecurityContext for GovernanceVault --- src/CommunityVault.sol | 4 ++-- src/GovernanceVault.sol | 4 ++-- test/Voting.t.sol | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/CommunityVault.sol b/src/CommunityVault.sol index ea8e577..92daf1f 100644 --- a/src/CommunityVault.sol +++ b/src/CommunityVault.sol @@ -63,7 +63,7 @@ contract CommunityVault is HasSecurityContext { * @param to The address to send the tokens or ETH to * @param amount The amount to withdraw */ - function withdraw(address token, address to, uint256 amount) external onlyRole(Roles.ADMIN_ROLE) { + function withdraw(address token, address to, uint256 amount) external onlyRole(Roles.SYSTEM_ROLE) { require(tokenBalances[token] >= amount, "Insufficient balance"); if (token == address(0)) { @@ -90,7 +90,7 @@ contract CommunityVault is HasSecurityContext { address token, address[] calldata recipients, uint256[] calldata amounts - ) external onlyRole(Roles.ADMIN_ROLE) { + ) external onlyRole(Roles.SYSTEM_ROLE) { require(recipients.length == amounts.length, "Mismatched arrays"); for (uint256 i = 0; i < recipients.length; i++) { diff --git a/src/GovernanceVault.sol b/src/GovernanceVault.sol index 83e683f..7ede8cf 100644 --- a/src/GovernanceVault.sol +++ b/src/GovernanceVault.sol @@ -80,7 +80,7 @@ contract GovernanceVault is HasSecurityContext { } // distribute rewards to a staker - function distributeRewards(address staker) public { + function distributeRewards(address staker) public onlyRole(Roles.SYSTEM_ROLE) { uint256 totalReward; Deposit[] storage userDeposits = deposits[staker]; @@ -97,7 +97,7 @@ contract GovernanceVault is HasSecurityContext { } // admin function to set the community vault address - function setCommunityVault(address _communityVault) external { + function setCommunityVault(address _communityVault) external onlyRole(Roles.SYSTEM_ROLE) { require(_communityVault != address(0), "Invalid address"); communityVault = _communityVault; } diff --git a/test/Voting.t.sol b/test/Voting.t.sol index 2d556e4..e55c7d7 100644 --- a/test/Voting.t.sol +++ b/test/Voting.t.sol @@ -11,6 +11,7 @@ import "@openzeppelin/contracts/governance/TimelockController.sol"; import { HamzaGovernor } from "../src/HamzaGovernor.sol"; import { Hats } from "@hats-protocol/Hats.sol"; +//TODO: inherit from DeploymentSetup contract VotingTest is Test { HatsSecurityContext securityContext; GovernanceToken govToken; From bb13e2880b13aad45af9ccfb61aa071ae59c011d Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 14:01:21 +0700 Subject: [PATCH 10/13] SecurityContext for PurchaseTracker --- scripts/DeployHamzaVault.s.sol | 5 +++-- src/PurchaseTracker.sol | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/DeployHamzaVault.s.sol b/scripts/DeployHamzaVault.s.sol index 929e145..1474b4d 100644 --- a/scripts/DeployHamzaVault.s.sol +++ b/scripts/DeployHamzaVault.s.sol @@ -135,7 +135,7 @@ contract DeployHamzaVault is Script { address timelockAddr = deployGovernorAndTimelock(govTokenAddr); // 15-16) Deploy PurchaseTracker, PaymentEscrow, and EscrowMulticall - deployEscrowContracts(vault, lootTokenAddr); + deployEscrowContracts(ISecurityContext(hatsSecurityContextAddr), vault, lootTokenAddr); vm.stopBroadcast(); @@ -405,13 +405,14 @@ contract DeployHamzaVault is Script { } function deployEscrowContracts( + ISecurityContext securityContext, address payable vault, address lootTokenAddr ) internal { bool autoRelease = config.readBool(".escrow.autoRelease"); // 15) Deploy PurchaseTracker - PurchaseTracker purchaseTracker = new PurchaseTracker(vault, lootTokenAddr); + PurchaseTracker purchaseTracker = new PurchaseTracker(securityContext, vault, lootTokenAddr); //setPurchaseTracker in community vault CommunityVault(vault).setPurchaseTracker(address(purchaseTracker), lootTokenAddr); diff --git a/src/PurchaseTracker.sol b/src/PurchaseTracker.sol index f9f53d8..dbb4b46 100644 --- a/src/PurchaseTracker.sol +++ b/src/PurchaseTracker.sol @@ -3,13 +3,14 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@hamza-escrow/security/HasSecurityContext.sol"; /** * @title PurchaseTracker * @notice A singleton contract that records purchase data. * */ -contract PurchaseTracker { +contract PurchaseTracker is HasSecurityContext { using SafeERC20 for IERC20; address public owner; @@ -55,10 +56,11 @@ contract PurchaseTracker { _; } - constructor(address _communityVault, address _lootToken) { + constructor(ISecurityContext securityContext, address _communityVault, address _lootToken) { owner = msg.sender; communityVault = _communityVault; lootToken = IERC20(_lootToken); + _setSecurityContext(securityContext); } /** From 3721f0dd30a10bac3f1b3d0fa147f7cf2351f00e Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 14:08:53 +0700 Subject: [PATCH 11/13] added role protection to authorizeEscrow/deauthorizeEscrow --- src/PurchaseTracker.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PurchaseTracker.sol b/src/PurchaseTracker.sol index dbb4b46..b4ace44 100644 --- a/src/PurchaseTracker.sol +++ b/src/PurchaseTracker.sol @@ -67,7 +67,7 @@ contract PurchaseTracker is HasSecurityContext { * @notice Authorizes an escrow (or other) contract to record purchases. * @param escrow The address of the escrow contract. */ - function authorizeEscrow(address escrow) external onlyOwner { + function authorizeEscrow(address escrow) external onlyRole(Roles.SYSTEM_ROLE) { authorizedEscrows[escrow] = true; } @@ -75,7 +75,7 @@ contract PurchaseTracker is HasSecurityContext { * @notice Removes authorization for an escrow contract. * @param escrow The address to deauthorize. */ - function deauthorizeEscrow(address escrow) external onlyOwner { + function deauthorizeEscrow(address escrow) external onlyRole(Roles.SYSTEM_ROLE) { authorizedEscrows[escrow] = false; } From 5676676dfc265a69306b7f8b9403312c2ad55405 Mon Sep 17 00:00:00 2001 From: Tronky Date: Thu, 6 Mar 2025 14:11:38 +0700 Subject: [PATCH 12/13] removed owner from PurchaseTracker --- src/PurchaseTracker.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/PurchaseTracker.sol b/src/PurchaseTracker.sol index b4ace44..78ee9fa 100644 --- a/src/PurchaseTracker.sol +++ b/src/PurchaseTracker.sol @@ -12,7 +12,6 @@ import "@hamza-escrow/security/HasSecurityContext.sol"; */ contract PurchaseTracker is HasSecurityContext { using SafeERC20 for IERC20; - address public owner; // Mapping from buyer address to cumulative purchase count and total purchase amount. mapping(address => uint256) public totalPurchaseCount; @@ -46,18 +45,12 @@ contract PurchaseTracker is HasSecurityContext { event PurchaseRecorded(bytes32 indexed paymentId, address indexed buyer, uint256 amount); - modifier onlyOwner() { - require(msg.sender == owner, "PurchaseTracker: Not owner"); - _; - } - modifier onlyAuthorized() { require(authorizedEscrows[msg.sender], "PurchaseTracker: Not authorized"); _; } constructor(ISecurityContext securityContext, address _communityVault, address _lootToken) { - owner = msg.sender; communityVault = _communityVault; lootToken = IERC20(_lootToken); _setSecurityContext(securityContext); From 4fa5e36ba45bb821f193fc9301f0f5231193a4b8 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Thu, 6 Mar 2025 15:52:39 -0600 Subject: [PATCH 13/13] Updated hamza-escrow submodule to main --- lib/hamza-escrow | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/hamza-escrow b/lib/hamza-escrow index 39e4403..fc0e1f2 160000 --- a/lib/hamza-escrow +++ b/lib/hamza-escrow @@ -1 +1 @@ -Subproject commit 39e440331ae4bfbff1be2205409947af6e323f13 +Subproject commit fc0e1f2a78469bcb416038a2e95311c8fb30c300