From a9a1195b17069f2584ba74476cce91eb331f074f Mon Sep 17 00:00:00 2001 From: Michael Montour Date: Mon, 16 Dec 2024 16:55:42 -0800 Subject: [PATCH] feat: Restructure HCHelper to be deployed with an upgradeable proxy. Add a maxCredit limit to the payment mechanism. Changes to be committed: new file: crates/types/contracts/hc_scripts/CoreDeploy_v7.s.sol modified: crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol modified: crates/types/contracts/src/hc0_7/HCHelper.sol modified: hybrid-compute/deploy-local.py --- .../contracts/hc_scripts/CoreDeploy_v7.s.sol | 74 +++++++++++++++++++ .../contracts/hc_scripts/LocalDeploy_v7.s.sol | 10 ++- crates/types/contracts/src/hc0_7/HCHelper.sol | 57 +++++++++++--- hybrid-compute/deploy-local.py | 2 +- 4 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 crates/types/contracts/hc_scripts/CoreDeploy_v7.s.sol diff --git a/crates/types/contracts/hc_scripts/CoreDeploy_v7.s.sol b/crates/types/contracts/hc_scripts/CoreDeploy_v7.s.sol new file mode 100644 index 0000000..57a56e4 --- /dev/null +++ b/crates/types/contracts/hc_scripts/CoreDeploy_v7.s.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +// forge script --json --broadcast --via-ir --rpc-url --contracts src/hc0_7 \ +// --remappings @openzeppelin/=lib/openzeppelin-contracts-versions/v5_0 +// --verifier-url \ +// hc_scripts/CoreDeploy_v7.sol + +pragma solidity ^0.8.23; + +import "forge-std/Script.sol"; +import "lib/account-abstraction-versions/v0_7/contracts/core/EntryPoint.sol"; +import "src/hc0_7/HCHelper.sol"; +import "src/hc0_7/HybridAccountFactory.sol"; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract CoreDeploy is Script { + function run() external + returns (address[4] memory) { + address deployAddr = vm.envAddress("DEPLOY_ADDR"); + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address hcSysOwner = vm.envAddress("HC_SYS_OWNER"); + uint256 deploySalt = vm.envUint("DEPLOY_SALT"); + + EntryPoint ept; + HCHelper helper; + HybridAccountFactory haf; + HybridAccount ha0; + + bytes32 salt_val = bytes32(deploySalt); + uint112 min_deposit = 0.001 ether; + + vm.startBroadcast(deployerPrivateKey); + + // EntryPointAddr is hard-coded for the v0.7 implementation + ept = EntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); + + HCHelper helperImpl = new HCHelper{salt: salt_val}(address(ept)); + + TransparentUpgradeableProxy hProxy = new TransparentUpgradeableProxy{salt: salt_val}( + address(helperImpl), + hcSysOwner, + abi.encodeCall(HCHelper.initialize, (deployAddr)) + ); + helper = HCHelper(address(hProxy)); + + { + address hafAddr = vm.envOr("HA_FACTORY_ADDR", 0x0000000000000000000000000000000000000000); + if (hafAddr != address(0) && hafAddr.code.length > 0) { + haf = HybridAccountFactory(hafAddr); + } else { + haf = new HybridAccountFactory{salt: salt_val}(ept, address(helper)); + } + } + { + address ha0Addr = vm.envOr("HC_SYS_ACCOUNT", 0x0000000000000000000000000000000000000000); + if (ha0Addr != address(0) && ha0Addr.code.length > 0) { + ha0 = HybridAccount(payable(ha0Addr)); + } else { + ha0 = haf.createAccount(hcSysOwner,0); + } + } + if (helper.systemAccount() != address(ha0)) { + helper.SetSystemAccount(address(ha0)); + } + + // Previous version deposited to EntryPoint, here we fund the acct directly + if (address(ha0).balance < min_deposit) { + payable(address(ha0)).transfer(min_deposit - address(ha0).balance); + } + + vm.stopBroadcast(); + return [address(ept),address(helper), address(haf), address(ha0)]; + } +} diff --git a/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol b/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol index 6364214..1e946a4 100644 --- a/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol +++ b/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol @@ -6,6 +6,7 @@ import "lib/account-abstraction-versions/v0_7/contracts/core/EntryPoint.sol"; import "src/hc0_7/HCHelper.sol"; import "src/hc0_7/HybridAccountFactory.sol"; import "lib/account-abstraction-versions/v0_7/contracts/samples/SimpleAccountFactory.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; contract LocalDeploy is Script { function run() external @@ -37,7 +38,14 @@ contract LocalDeploy is Script { if (helperAddr != address(0) && helperAddr.code.length > 0) { helper = HCHelper(helperAddr); } else { - helper = new HCHelper{salt: salt_val}(address(ept), bobaAddr, deployAddr); + HCHelper helperImpl = new HCHelper{salt: salt_val}(address(ept)); + + TransparentUpgradeableProxy hProxy = new TransparentUpgradeableProxy( + address(helperImpl), + hcSysOwner, + abi.encodeCall(HCHelper.initialize, (deployAddr)) + ); + helper = HCHelper(address(hProxy)); } } { diff --git a/crates/types/contracts/src/hc0_7/HCHelper.sol b/crates/types/contracts/src/hc0_7/HCHelper.sol index ca74ebe..e5ba83e 100644 --- a/crates/types/contracts/src/hc0_7/HCHelper.sol +++ b/crates/types/contracts/src/hc0_7/HCHelper.sol @@ -4,9 +4,10 @@ pragma solidity ^0.8.12; import "account-abstraction/v0_7/interfaces/INonceManager.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -contract HCHelper is ReentrancyGuard, Ownable { +contract HCHelper is ReentrancyGuard, UUPSUpgradeable, Initializable { using SafeERC20 for IERC20; event SystemAccountSet(address oldAccount, address newAccount); @@ -16,15 +17,26 @@ contract HCHelper is ReentrancyGuard, Ownable { // Response data is stored here by PutResponse() and then consumed by TryCallOffchain(). mapping(bytes32=>bytes) ResponseCache; + // AA EntryPoint + address public immutable entryPoint; + + // Owner + address public owner; + + // Account which is used to insert system error responses. Currently a single + // address but could be extended to a list of authorized accounts if needed. + address public systemAccount; + // BOBA token address address public tokenAddr; // Token amount required to purchase each prepaid credit (may be 0 for testing) uint256 public pricePerCall; - // Account which is used to insert system error responses. Currently a single - // address but could be extended to a list of authorized accounts if needed. - address public systemAccount; + // Limit on the maximum credit balance which an account may hold, enforced + // when purchasing credits. This allows system testing or temporary promotions + // with a low or zero credit price. + uint64 public maxCredits; // Data stored per RegisteredCaller struct callerInfo { @@ -36,13 +48,29 @@ contract HCHelper is ReentrancyGuard, Ownable { // Contracts which are allowed to use Hybrid Compute. mapping(address=>callerInfo) public RegisteredCallers; - // AA EntryPoint - address immutable entryPoint; + + modifier onlyOwner() { + _onlyOwner(); + _; + } + function _onlyOwner() internal view { + require(msg.sender == owner || msg.sender == address(this), "only owner"); + } // Constructor - constructor(address _entryPoint, address _tokenAddr, address _owner) Ownable(_owner) { + constructor(address _entryPoint) { entryPoint = _entryPoint; - tokenAddr = _tokenAddr; + } + + // Set the initial owner + function initialize(address _owner) public virtual initializer { + owner = _owner; + } + + // Allow upgrade through UUPSUpgradeable + function _authorizeUpgrade(address newImplementation) internal view override { + (newImplementation); + _onlyOwner(); } // Change the SystemAccount address (used for error responses) @@ -59,17 +87,22 @@ contract HCHelper is ReentrancyGuard, Ownable { emit RegisteredUrl(contract_addr, url); } - // Set or change the per-call token price (0 is allowed). Does not affect - // existing credit balances, only applies to new AddCredit() calls. - function SetPrice(uint256 _pricePerCall) public onlyOwner { + // Set or change the per-call token price (0 is allowed), token, + // and maximum credit balance. Does not affect existing balances, + // only new AddCredit() purchases. + function SetPaymentInfo(address _tokenAddr, uint256 _pricePerCall, uint64 _maxCredits) public onlyOwner { + tokenAddr = _tokenAddr; pricePerCall = _pricePerCall; + maxCredits = _maxCredits; } // Purchase credits allowing the specified contract to perform HC calls. // The token cost is (pricePerCall() * numCredits) and is non-refundable function AddCredit(address contract_addr, uint256 numCredits) public nonReentrant { + require(tokenAddr != address(0), "Payment info not initialized"); uint256 tokenPrice = numCredits * pricePerCall; RegisteredCallers[contract_addr].credits += numCredits; + require(RegisteredCallers[contract_addr].credits <= maxCredits, "Purchase exceeds maxCredits limit"); IERC20(tokenAddr).safeTransferFrom(msg.sender, address(this), tokenPrice); } diff --git a/hybrid-compute/deploy-local.py b/hybrid-compute/deploy-local.py index ee76958..d6a35a4 100644 --- a/hybrid-compute/deploy-local.py +++ b/hybrid-compute/deploy-local.py @@ -389,7 +389,7 @@ def boba_balance(addr): HH = load_contract(w3, 'HCHelper', OUT_PREFIX + "HCHelper.sol/HCHelper.json", hh_addr) l2_util.approve_token(boba_token, HH.address, deploy_addr, deploy_key) -tx = HH.functions.SetPrice(Web3.to_wei(0.1,'ether')). build_transaction({ +tx = HH.functions.SetPaymentInfo(boba_token, Web3.to_wei(0.1,'ether'), 1000000). build_transaction({ 'from': deploy_addr, }) l2_util.sign_and_submit(tx, deploy_key)