diff --git a/contracts/v1.5.x/BlastConstant.sol b/contracts/v1.5.x/BlastConstant.sol new file mode 100644 index 0000000..9739c23 --- /dev/null +++ b/contracts/v1.5.x/BlastConstant.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +// blast yield contract +IBlast constant BLAST = IBlast(0x4300000000000000000000000000000000000002); +// BlastGasCollector contract using Create3Factory to generate constant contract address +address constant GAS_COLLECTOR = 0xBd9D6d96b21d679983Af4ed6182Fd9fff0031eA4; + +interface IBlast { + // see https://docs.blast.io/building/guides/gas-fees + function configureAutomaticYield() external; + function configureClaimableGas() external; + function configureGovernor(address governor) external; + function configureGovernorOnBehalf(address _newGovernor, address contractAddress) external; + function claimAllGas(address contractAddress, address recipient) external returns (uint256); +} + +interface IBlastPoints { + function configurePointsOperator(address operator) external; +} diff --git a/contracts/v1.5.x/BlastGasCollector.sol b/contracts/v1.5.x/BlastGasCollector.sol new file mode 100644 index 0000000..bfab1d5 --- /dev/null +++ b/contracts/v1.5.x/BlastGasCollector.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import {BLAST} from "./BlastConstant.sol"; + +contract BlastGasCollector is AccessControl { + /// @notice gas collector role for using claimGas() + bytes32 public constant GAS_COLLECTOR_ROLE = keccak256("GAS_COLLECTOR_ROLE"); + + /// @notice constructor setting owner and configure BLAST + constructor(address _admin) { + // contract balance will grow automatically + BLAST.configureAutomaticYield(); + // let GAS_COLLECTOR collect gas + BLAST.configureClaimableGas(); + // sender as default admin + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + } + + /// @notice can claim gas including self gas + /// @param target claim target + /// @param recipientOfGas claimed gas recipientOfGas + function claimGas(address target, address recipientOfGas) external { + require(hasRole(GAS_COLLECTOR_ROLE, msg.sender), "caller is not gas collecotr role"); + BLAST.claimAllGas(target, recipientOfGas); + } + + /// @notice can claim gas including self gas + /// @param targetAry claim target + /// @param recipientOfGas claimed gas recipientOfGas + function claimGasBatch(address[] calldata targetAry, address recipientOfGas) external { + require(hasRole(GAS_COLLECTOR_ROLE, msg.sender), "caller is not gas collecotr role"); + for (uint256 i = 0; i < targetAry.length; i++) { + BLAST.claimAllGas(targetAry[i], recipientOfGas); + } + } + + /// @notice configures the governor for a specific contract. Called by an authorized user + /// @param _newGovernor the address of new governor + /// @param target the address of the contract to be configured + function configureGovernorOnBehalf(address _newGovernor, address target) external { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not admin"); + BLAST.configureGovernorOnBehalf(_newGovernor, target); + } + + /// @notice configures the governor for a specific contract. Called by an authorized user + /// @param _newGovernor the address of new governor + /// @param targetAry the address of the contract to be configured + function configureGovernorOnBehalf(address _newGovernor, address[] calldata targetAry) external { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not admin"); + for (uint256 i = 0; i < targetAry.length; i++) { + BLAST.configureGovernorOnBehalf(_newGovernor, targetAry[i]); + } + } + + /// @notice selfdestruct to transfer all funds to the owner + function destruct() external { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not admin"); + selfdestruct(payable(msg.sender)); + } +} diff --git a/contracts/v1.5.x/BloctoAccount.sol b/contracts/v1.5.x/BloctoAccount.sol index a82f36f..c4b877e 100644 --- a/contracts/v1.5.x/BloctoAccount.sol +++ b/contracts/v1.5.x/BloctoAccount.sol @@ -10,6 +10,7 @@ import "@account-abstraction/contracts/core/BaseAccount.sol"; import "../utils/TokenCallbackHandler.sol"; import "./CoreWallet.sol"; +import {BLAST, IBlastPoints, GAS_COLLECTOR} from "./BlastConstant.sol"; /** * Blocto account. @@ -24,6 +25,9 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas /// @notice entrypoint from 4337 official IEntryPoint private immutable _entryPoint; + /// @notice blast points contract address, testnet address DIFF from mainnet + IBlastPoints public immutable _blastPoints; + /// @notice initialized _IMPLEMENTATION_SLOT bool public initializedImplementation = false; @@ -31,8 +35,9 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas * constructor for BloctoAccount * @param anEntryPoint entrypoint address */ - constructor(IEntryPoint anEntryPoint) { + constructor(IEntryPoint anEntryPoint, address blastPointsAddr) { _entryPoint = anEntryPoint; + _blastPoints = IBlastPoints(blastPointsAddr); } /** @@ -111,20 +116,6 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas return 0; } - /** - * check current account deposit in the entryPoint StakeManager - */ - function getDeposit() public view returns (uint256) { - return entryPoint().balanceOf(address(this)); - } - - /** - * deposit more funds for this account in the entryPoint StakeManager - */ - function addDeposit() public payable { - entryPoint().depositTo{value: msg.value}(address(this)); - } - /** * withdraw deposit to withdrawAddress from entryPoint StakeManager * @param withdrawAddress target to send to @@ -147,4 +138,17 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas require(Address.isContract(implementation), "ERC1967: new implementation is not a contract"); StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = implementation; } + + /// @notice configure blast for yield, gas, and points + /// @param pointsOperator blast points contract operator address, should be EOA from https://docs.blast.io/airdrop/api#configuring-a-points-operator + function configureBlast(address pointsOperator) external { + require(!initialized, "blast: must not be initialized"); + // contract balance will grow automatically + BLAST.configureAutomaticYield(); + // let GAS_COLLECTOR collect gas + BLAST.configureClaimableGas(); + BLAST.configureGovernor(GAS_COLLECTOR); + // operator should be EOA + _blastPoints.configurePointsOperator(pointsOperator); + } } diff --git a/contracts/v1.5.x/BloctoAccountCloneableWallet.sol b/contracts/v1.5.x/BloctoAccountCloneableWallet.sol index aca33b0..1198bdc 100644 --- a/contracts/v1.5.x/BloctoAccountCloneableWallet.sol +++ b/contracts/v1.5.x/BloctoAccountCloneableWallet.sol @@ -8,7 +8,8 @@ import "./BloctoAccount.sol"; contract BloctoAccountCloneableWallet is BloctoAccount { /// @notice constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` /// @param anEntryPoint entrypoint address - constructor(IEntryPoint anEntryPoint) BloctoAccount(anEntryPoint) { + /// @param blastPointsAddr blast points contract address + constructor(IEntryPoint anEntryPoint, address blastPointsAddr) BloctoAccount(anEntryPoint, blastPointsAddr) { initialized = true; initializedImplementation = true; } diff --git a/contracts/v1.5.x/BloctoAccountFactoryBase.sol b/contracts/v1.5.x/BloctoAccountFactoryBase.sol index 0f012ba..7dd359c 100644 --- a/contracts/v1.5.x/BloctoAccountFactoryBase.sol +++ b/contracts/v1.5.x/BloctoAccountFactoryBase.sol @@ -8,6 +8,8 @@ import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import "../BloctoAccountProxy.sol"; import "./BloctoAccount.sol"; +import {BLAST, IBlastPoints, GAS_COLLECTOR} from "./BlastConstant.sol"; + // BloctoAccountFactory for creating BloctoAccountProxy contract BloctoAccountFactoryBase is Initializable, AccessControlUpgradeable { /// @notice create account role for using createAccount() and createAccount2() @@ -49,6 +51,19 @@ contract BloctoAccountFactoryBase is Initializable, AccessControlUpgradeable { bloctoAccountImplementation = _bloctoAccountImplementation; entryPoint = _entryPoint; _setupRole(DEFAULT_ADMIN_ROLE, _admin); + + // contract balance will grow automatically + BLAST.configureAutomaticYield(); + // let GAS_COLLECTOR collect gas + BLAST.configureClaimableGas(); + BLAST.configureGovernor(GAS_COLLECTOR); + } + + /// @notice configure blast for yield, gas, and points + /// @param pointsOperator blast points contract operator address, should be EOA from https://docs.blast.io/airdrop/api#configuring-a-points-operator + function configureBlastPoints(address blastPointsAddr, address pointsOperator) external onlyAdmin { + // operator should be EOA + IBlastPoints(blastPointsAddr).configurePointsOperator(pointsOperator); } /// @notice only the admin can update admin functioins diff --git a/contracts/v1.5.x/BloctoAccountFactoryV1_5_2.sol b/contracts/v1.5.x/BloctoAccountFactoryV1_5_2.sol index 2c1fdd2..7e2e53c 100644 --- a/contracts/v1.5.x/BloctoAccountFactoryV1_5_2.sol +++ b/contracts/v1.5.x/BloctoAccountFactoryV1_5_2.sol @@ -27,6 +27,7 @@ contract BloctoAccountFactoryV1_5_2 is BloctoAccountFactoryBase { ); ret = BloctoAccount(payable(newProxy)); ret.initImplementation(bloctoAccountImplementation); + ret.configureBlast(msg.sender); ret.init( _authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParity, _mergedKey ); @@ -55,6 +56,7 @@ contract BloctoAccountFactoryV1_5_2 is BloctoAccountFactoryBase { ); ret = BloctoAccount(payable(newProxy)); ret.initImplementation(bloctoAccountImplementation); + ret.configureBlast(msg.sender); ret.init2( _authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParitys, _mergedKeys ); @@ -98,6 +100,7 @@ contract BloctoAccountFactoryV1_5_2 is BloctoAccountFactoryBase { Create2.deploy(0, _salt, abi.encodePacked(BLOCTO_ACCOUNT_PROXY, abi.encode(address(initImplementation)))); ret = BloctoAccount(payable(newProxy)); ret.initImplementation(bloctoAccountImplementation151Plus); + ret.configureBlast(msg.sender); ret.init( _authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParity, _mergedKey ); @@ -124,6 +127,7 @@ contract BloctoAccountFactoryV1_5_2 is BloctoAccountFactoryBase { Create2.deploy(0, _salt, abi.encodePacked(BLOCTO_ACCOUNT_PROXY, abi.encode(address(initImplementation)))); ret = BloctoAccount(payable(newProxy)); ret.initImplementation(bloctoAccountImplementation151Plus); + ret.configureBlast(msg.sender); ret.init2( _authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParitys, _mergedKeys ); diff --git a/contracts/v1.5.x/BloctoAccountFactoryV1_5_3.sol b/contracts/v1.5.x/BloctoAccountFactoryV1_5_3.sol index 0b5089e..3bad044 100644 --- a/contracts/v1.5.x/BloctoAccountFactoryV1_5_3.sol +++ b/contracts/v1.5.x/BloctoAccountFactoryV1_5_3.sol @@ -32,6 +32,7 @@ contract BloctoAccountFactoryV1_5_3 is BloctoAccountFactoryBase { Create2.deploy(0, _salt, abi.encodePacked(BLOCTO_ACCOUNT_PROXY, abi.encode(address(initImplementation)))); ret = BloctoAccount(payable(newProxy)); ret.initImplementation(bloctoAccountImplementation_1_5_3); + ret.configureBlast(msg.sender); ret.init( _authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParity, _mergedKey ); @@ -58,6 +59,7 @@ contract BloctoAccountFactoryV1_5_3 is BloctoAccountFactoryBase { Create2.deploy(0, _salt, abi.encodePacked(BLOCTO_ACCOUNT_PROXY, abi.encode(address(initImplementation)))); ret = BloctoAccount(payable(newProxy)); ret.initImplementation(bloctoAccountImplementation_1_5_3); + ret.configureBlast(msg.sender); ret.init2( _authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParitys, _mergedKeys ); diff --git a/contracts/v1.5.x/CoreWallet.sol b/contracts/v1.5.x/CoreWallet.sol index 8cbc309..2dceed4 100644 --- a/contracts/v1.5.x/CoreWallet.sol +++ b/contracts/v1.5.x/CoreWallet.sol @@ -245,7 +245,6 @@ contract CoreWallet is IERC1271 { require(_authorizedAddress != _recoveryAddress, "do not use the recovery address as an authorized address"); authorizations[AUTH_VERSION_INCREMENTOR + uint256(uint160(_authorizedAddress))] = _cosigner; mergedKeys[AUTH_VERSION_INCREMENTOR + _mergedKeyIndexWithParitys[i]] = _mergedKeys[i]; - emit Authorized(_authorizedAddress, _cosigner); } } diff --git a/deploy/1_0_deploy_account_accountFactory.ts b/deploy/1_0_deploy_account_accountFactory.ts index 99609d7..bd842cd 100644 --- a/deploy/1_0_deploy_account_accountFactory.ts +++ b/deploy/1_0_deploy_account_accountFactory.ts @@ -23,6 +23,12 @@ const Create3FactoryAddress = '0x2f06F83f960ea999536f94df279815F79EeB4054' const BloctoAccountCloneableWalletSalt = 'BloctoAccount_v140' const BloctoAccountFactorySalt = 'BloctoAccountFactoryProxy_v140' +async function getBlastPointAddress (): Promise { + const { chainId } = await ethers.provider.getNetwork() + // 81457: mainnet using 0x2536FE9ab3F511540F2f9e2eC2A805005C3Dd800 from https://docs.blast.io/airdrop/api#configuring-a-points-operator + return chainId === 81457 ? '0x2536FE9ab3F511540F2f9e2eC2A805005C3Dd800' : '0x2fc95838c71e76ec69ff817983BFf17c710F34E0' +} + async function main (): Promise { // const lockedAmount = ethers.utils.parseEther("1"); const [owner] = await ethers.getSigners() @@ -34,11 +40,14 @@ async function main (): Promise { console.log(`Deploying BloctoAccountCloneableWallet with -> \n\t salt str: ${BloctoAccountCloneableWalletSalt}`) const walletCloneable = await create3Factory.getDeployed(owner.address, accountSalt) + const blastPointAddress = await getBlastPointAddress() + console.log('Using blastPointAddress: ', blastPointAddress) + if ((await ethers.provider.getCode(walletCloneable)) === '0x') { console.log(`BloctowalletCloneableWallet deploying to: ${walletCloneable}`) const tx = await create3Factory.deploy( accountSalt, - getDeployCode(new BloctoAccountCloneableWallet__factory(), [EntryPoint])) + getDeployCode(new BloctoAccountCloneableWallet__factory(), [EntryPoint, blastPointAddress])) await tx.wait() console.log(`BloctowalletCloneableWallet JUST deployed to: ${walletCloneable}`) @@ -54,7 +63,7 @@ async function main (): Promise { const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') const accountFactory = await create3DeployTransparentProxy(BloctoAccountFactory, [walletCloneable, EntryPoint, owner.address], - { initializer: 'initialize' }, create3Factory, owner, accountFactorySalt) + { initializer: 'initialize', constructorArgs: [walletCloneable], unsafeAllow: ['constructor', 'state-variable-immutable'] }, create3Factory, owner, accountFactorySalt) await accountFactory.deployed() console.log(`BloctoAccountFactory JUST deployed to: ${accountFactory.address}`) @@ -63,6 +72,9 @@ async function main (): Promise { await accountFactory.grantRole(await accountFactory.CREATE_ACCOUNT_ROLE(), CreateAccountBackend) console.log('setImplementation_1_5_1 to address: ', walletCloneable) await accountFactory.setImplementation_1_5_1(walletCloneable) + console.log('set blast point to address: ', CreateAccountBackend) + const blastPointAddress = await getBlastPointAddress() + await accountFactory.configureBlastPoints(blastPointAddress, CreateAccountBackend) } else { console.log(`BloctoAccountFactory WAS deployed to: ${accountFactoryAddr}`) } @@ -86,7 +98,7 @@ async function main (): Promise { address: walletCloneable, contract: 'contracts/v1.5.x/BloctoAccountCloneableWallet.sol:BloctoAccountCloneableWallet', constructorArguments: [ - EntryPoint + EntryPoint, blastPointAddress ] }) @@ -94,7 +106,10 @@ async function main (): Promise { const accountFactoryImplAddress = await getImplementationAddress(ethers.provider, accountFactoryAddr) await hre.run('verify:verify', { address: accountFactoryImplAddress, - contract: 'contracts/v1.5.x/BloctoAccountFactory.sol:BloctoAccountFactory' + contract: 'contracts/v1.5.x/BloctoAccountFactory.sol:BloctoAccountFactory', + constructorArguments: [ + walletCloneable + ] }) } diff --git a/deploy/3_2_createMultipleSchnorrAccount.ts b/deploy/3_2_createMultipleSchnorrAccount.ts index f12efcd..fa5ee36 100644 --- a/deploy/3_2_createMultipleSchnorrAccount.ts +++ b/deploy/3_2_createMultipleSchnorrAccount.ts @@ -38,7 +38,7 @@ async function main (): Promise { console.log('ethersSigner address: ', await ethersSigner.getAddress()) console.log('factory.address', factory.address) - const tx = await factory.createAccount2([authorizedWallet.address, authorizedWallet2.address, cosignerWallet.address], + const tx = await factory.createAccount2_1_5_3([authorizedWallet.address, authorizedWallet2.address, cosignerWallet.address], cosignerWallet.address, RecoverAddress, SALT, // random salt [pxIndexWithParity, pxIndexWithParity2, pxIndexWithParity3], diff --git a/deploy/6_deploy_BlastGasCollector.ts b/deploy/6_deploy_BlastGasCollector.ts new file mode 100644 index 0000000..2ce1736 --- /dev/null +++ b/deploy/6_deploy_BlastGasCollector.ts @@ -0,0 +1,68 @@ +import hre, { ethers } from 'hardhat' +import { getDeployCode } from '../src/create3Factory' +import { + BlastGasCollector__factory, + CREATE3Factory__factory +} from '../typechain' +import { hexZeroPad } from '@ethersproject/bytes' + +// prod mainnet +// const CreateAccountBackend = '0x8A6a17F1A3DA0F407A67BF8E076Ed7F678D85f29' +// dev testnet +// const GasCollectorBackend = '0x67465ec61c3c07b119e09fbb4a0b59eb1ba14e62' +// prod mainnet +const GasCollectorBackend = '0x8A6a17F1A3DA0F407A67BF8E076Ed7F678D85f29' + +// create3Factory +const Create3FactoryAddress = '0x2f06F83f960ea999536f94df279815F79EeB4054' + +// BloctocontractInstanceSalt +const BlastGasCollectorSalt = 'BlastGasCollector_v1.0' + +async function main (): Promise { + // const lockedAmount = ethers.utils.parseEther("1"); + const [owner] = await ethers.getSigners() + console.log('deploy with account: ', owner.address) + + const create3Factory = CREATE3Factory__factory.connect(Create3FactoryAddress, owner) + // -------------------BlastGasCollector------------------------------// + const contractSalt = hexZeroPad(Buffer.from(BlastGasCollectorSalt, 'utf-8'), 32) + console.log(`Deploying BlastGasCollector with -> \n\t salt str: ${BlastGasCollectorSalt}`) + const contractInstance = await create3Factory.getDeployed(owner.address, contractSalt) + + if ((await ethers.provider.getCode(contractInstance)) === '0x') { + console.log(`BlastGasCollector deploying to: ${contractInstance}`) + const tx = await create3Factory.deploy( + contractSalt, + getDeployCode(new BlastGasCollector__factory(), [owner.address])) + await tx.wait() + console.log(`BlastGasCollector JUST deployed to: ${contractInstance}`) + console.log('Granting gas collector role to backend address: ', GasCollectorBackend) + + const gasCollector = BlastGasCollector__factory.connect(contractInstance, owner) + await gasCollector.grantRole(await gasCollector.GAS_COLLECTOR_ROLE(), GasCollectorBackend) + } else { + console.log(`BlastGasCollector WAS deployed to: ${contractInstance}`) + } + + // sleep 10 seconds + console.log('sleep 10 seconds for chain sync...') + await new Promise(f => setTimeout(f, 10000)) + + // -------------------Verify------------------------------// + // verify BlastGasCollector + await hre.run('verify:verify', { + address: contractInstance, + contract: 'contracts/v1.5.x/BlastGasCollector.sol:BlastGasCollector', + constructorArguments: [ + owner.address + ] + }) +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/deploy/upgrade/0_upgrade.ts b/deploy/upgrade/0_upgrade.ts index 880306e..8935fae 100644 --- a/deploy/upgrade/0_upgrade.ts +++ b/deploy/upgrade/0_upgrade.ts @@ -9,30 +9,42 @@ import { hexZeroPad } from '@ethersproject/bytes' import { getDeployCode } from '../../src/create3Factory' import { getImplementationAddress } from '@openzeppelin/upgrades-core' -const NextVersion = '1.5.3' +const NextVersion = '1.5.3-blast-0.1' // entrypoint from 4337 official (0.6.0) const EntryPoint = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' // mainnet -// const Create3FactoryAddress = '0x2f06F83f960ea999536f94df279815F79EeB4054' -// const BloctoAccountFactoryAddr = '0xF7cCFaee69cD8A0B3a62C2A0f35F95cC7e588183' +let Create3FactoryAddress = '0x2f06F83f960ea999536f94df279815F79EeB4054' +let BloctoAccountFactoryAddr = '0xF7cCFaee69cD8A0B3a62C2A0f35F95cC7e588183' // testnet -const Create3FactoryAddress = '0xd6CA621705575c3c23622b0802964a556870953b' -const BloctoAccountFactoryAddr = '0x38DDa3Aed6e71457d573F993ee06380b1cDaF3D1' + +async function getBlastPointAddress (): Promise { + const { chainId } = await ethers.provider.getNetwork() + // 81457: mainnet using 0x2536FE9ab3F511540F2f9e2eC2A805005C3Dd800 from https://docs.blast.io/airdrop/api#configuring-a-points-operator + return chainId === 81457 ? '0x2536FE9ab3F511540F2f9e2eC2A805005C3Dd800' : '0x2fc95838c71e76ec69ff817983BFf17c710F34E0' +} async function main (): Promise { const [owner] = await ethers.getSigners() console.log('upgrade with owner:', owner.address) + // testnet deployer + if (owner.address === '0x162235eBF3381eDE497dFa523b2a77E2941583eC') { + Create3FactoryAddress = '0xd6CA621705575c3c23622b0802964a556870953b' + BloctoAccountFactoryAddr = '0x38DDa3Aed6e71457d573F993ee06380b1cDaF3D1' + } const create3Factory = CREATE3Factory__factory.connect(Create3FactoryAddress, owner) // deploy BloctoAccount next version const nextVersionBloctoAccountCloneable = 'BloctoAccount_' + NextVersion const accountCloneableSalt = hexZeroPad(Buffer.from(nextVersionBloctoAccountCloneable, 'utf-8'), 32) const implementation = await create3Factory.getDeployed(await owner.getAddress(), accountCloneableSalt) + const blastPointAddress = await getBlastPointAddress() + console.log('Using blastPointAddress: ', blastPointAddress) + if ((await ethers.provider.getCode(implementation)) === '0x') { console.log(`BloctowalletCloneableWallet ${NextVersion} deploying to: ${implementation}`) const tx = await create3Factory.deploy( accountCloneableSalt, - getDeployCode(new BloctoAccountCloneableWallet__factory(), [EntryPoint]) + getDeployCode(new BloctoAccountCloneableWallet__factory(), [EntryPoint, blastPointAddress]) ) await tx.wait() console.log(`BloctowalletCloneableWallet ${NextVersion} JUST deployed to: ${implementation}`) @@ -61,7 +73,7 @@ async function main (): Promise { address: implementation, contract: 'contracts/v1.5.x/BloctoAccountCloneableWallet.sol:BloctoAccountCloneableWallet', constructorArguments: [ - EntryPoint + EntryPoint, blastPointAddress ] }) diff --git a/deploy/upgrade/0_upgrade_factory.ts b/deploy/upgrade/0_upgrade_factory.ts index 1d0f85f..4a36ec9 100644 --- a/deploy/upgrade/0_upgrade_factory.ts +++ b/deploy/upgrade/0_upgrade_factory.ts @@ -3,7 +3,7 @@ import hre, { ethers } from 'hardhat' import { getImplementationAddress } from '@openzeppelin/upgrades-core' const BloctoAccountFactoryAddr = '0x38DDa3Aed6e71457d573F993ee06380b1cDaF3D1' -const BloctoAccountCloneablelAddr = '0x77E262adD1b7DBF4ad7C39045CCC0FB22f060867' +const BloctoAccountCloneablelAddr = '0x89EbeBE2bA6638729FBD2F33d200A48C81684c3c' async function main (): Promise { const [owner] = await ethers.getSigners() diff --git a/hardhat.config.ts b/hardhat.config.ts index 66985b3..cad8079 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -18,7 +18,8 @@ const { BASESCAN_API_KEY, // base scan API KEY LINEASCAN_API_KEY, // linea scan API KEY BASE_SEPOLIA_API_KEY, // base sepolia scan API KEY - SCROLLSCAN_API_KEY // scroll scan API KEY + SCROLLSCAN_API_KEY, // scroll scan API KEY + BLASTSCAN_API_KEY // blast scan API KEY } = process.env function getDeployAccount (): string[] { @@ -198,6 +199,17 @@ const config: HardhatUserConfig = { url: 'https://rpc.startale.com/zkatana', accounts: getDeployAccount(), chainId: 1261120 + }, + blast_sepolia: { + url: 'https://blast-sepolia.blockpi.network/v1/rpc/public', + accounts: getDeployAccount(), + chainId: 168587773, + gasPrice: 3000000000 + }, + blast: { + url: 'https://rpc.ankr.com/blast', + accounts: getDeployAccount(), + chainId: 81457 } }, mocha: { @@ -231,7 +243,9 @@ const config: HardhatUserConfig = { scroll: SCROLLSCAN_API_KEY, scrollSepolia: SCROLLSCAN_API_KEY, astarZkevmSepolia: SCROLLSCAN_API_KEY, - taikoJolnirSepolia: SCROLLSCAN_API_KEY + taikoJolnirSepolia: SCROLLSCAN_API_KEY, + blast_sepolia: 'blast_sepolia', + blast: BLASTSCAN_API_KEY }, customChains: [ { @@ -345,6 +359,22 @@ const config: HardhatUserConfig = { apiURL: 'https://explorer.jolnir.taiko.xyz/api', browserURL: 'https://explorer.jolnir.taiko.xyz/' } + }, + { + network: 'blast_sepolia', + chainId: 168587773, + urls: { + apiURL: 'https://api.routescan.io/v2/network/testnet/evm/168587773/etherscan', + browserURL: 'https://testnet.blastscan.io' + } + }, + { + network: 'blast', + chainId: 81457, + urls: { + apiURL: 'https://api.blastscan.io/api', + browserURL: 'https://blastscan.io/' + } } ] } diff --git a/package.json b/package.json index 2d36cd6..564ee40 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "deploy-create3Factory": "hardhat run deploy/0_deploy_create3Factory.ts", "deploy-verifyingPaymaster": "hardhat run deploy/2_deploy_VerifyingPaymaster.ts", "deploy-upgrade": "hardhat run deploy/upgrade/0_upgrade.ts", - "deploy-verify": "hardhat run deploy/4_verify.ts" + "deploy-verify": "hardhat run deploy/4_verify.ts", + "deploy-blastGasCollector": "hardhat run deploy/6_deploy_BlastGasCollector.ts" }, "devDependencies": { "@account-abstraction/contracts": "^0.6.0", @@ -27,6 +28,7 @@ "@types/node": "^16.4.12", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", + "axios": "^1.6.7", "chai": "^4.3.4", "eslint": "^8.19.0", "eslint-config-standard": "^17.0.0", @@ -66,4 +68,4 @@ "typescript": "^4.3.5" }, "license": "GPL-3.0" -} \ No newline at end of file +} diff --git a/test/blast.test.ts b/test/blast.test.ts new file mode 100644 index 0000000..e0e4866 --- /dev/null +++ b/test/blast.test.ts @@ -0,0 +1,191 @@ +import { ethers } from 'hardhat' +import { Contract, BigNumber } from 'ethers' +import { expect } from 'chai' +import { + BlastGasCollector__factory +} from '../typechain' +import axios, { AxiosResponse } from 'axios' + +const BlastGasCollectorAddr = '0xBd9D6d96b21d679983Af4ed6182Fd9fff0031eA4' +const GasAddr = '0x4300000000000000000000000000000000000001' + +async function readGasCanBeClaimed (chkAddr: string): Promise<[BigNumber, number]> { + // see https://testnet.blastscan.io/address/0x4300000000000000000000000000000000000001/contract/168587773/code + + const gasAbi = [ + // Get the account balance + 'function readGasParams(address) view returns (uint256,uint256,uint256,uint)' + ] + + const gasContract = new Contract(GasAddr, gasAbi, ethers.provider) + // (uint256 etherSeconds, uint256 etherBalance, uint256 lastUpdated, GasMode mode) + const [, etherBalance, , mode] = await gasContract.readGasParams(chkAddr) + return [etherBalance, mode] +} + +describe('Blast Test', function () { + const ethersSigner = ethers.provider.getSigner(0) + describe('Blast Gas Collector Test', function () { + // can claim factory gas + // const targetAddr = '0xF7cCFaee69cD8A0B3a62C2A0f35F95cC7e588183' + // can claim wallet gas + const targetAddr = '0xB6cbD452647435971F5ddbE72D85808d06CBcD28' + + const gasCollecotr = BlastGasCollector__factory.connect(BlastGasCollectorAddr, ethersSigner) + + it('should collect gas if exist', async () => { + const [etherBalance, mode] = await readGasCanBeClaimed(targetAddr) + // sholue be 1 (CLAIMABLE mode), see https://testnet.blastscan.io/address/0x4300000000000000000000000000000000000001/contract/168587773/code + expect(mode).to.equal(1) + if (etherBalance.gt(0)) { + console.log(`collecting ${targetAddr} etherBalance ${etherBalance.toString()}...`) + const tx = await gasCollecotr.claimGas(targetAddr, await ethersSigner.getAddress()) + await tx.wait() + } else { + console.log('no gas to be collected of', targetAddr) + } + }) + }) + + describe('Blast Points Test', function () { + // follow https://docs.blast.io/airdrop/api Mainnet Points API + const APIBaseURL = 'https://waitlist-api.develop.testblast.io' + type PointType = 'LIQUIDITY' | 'DEVELOPER' + interface PointsByAsset { + ETH: AssetPoints + WETH: AssetPoints + USDB: AssetPoints + } + + interface AssetPoints { + // same semantics as PointBalances.earnedCumulative + // but specific to an asset + earnedCumulative: string // decimal string + // earnedCumulative is the sum of points earned + // from block 0 to earnedCumulativeBlock + earnedCumulativeBlock: number + } + interface PointBalances { + // decimal strings + available: string + pendingSent: string + + // also decimal strings + // cumulative so they don't decrease + // a batch may become finalized before these numbers update + earnedCumulative: string + receivedCumulative: string // received from transfers (finalized) + finalizedSentCumulative: string // sent from transfers (finalized) + } + + interface PointBalancesResponse { + success: boolean + balancesByPointType: { + LIQUIDITY: PointBalances & { byAsset: PointsByAsset } + DEVELOPER: PointBalances + } + } + + let bearerToken: string + async function obtainChallenge (contractAddress: string, operatorAddress: string): Promise<[string, string]> { + const requestPayload = { + contractAddress: contractAddress, + operatorAddress: operatorAddress + } + + interface Response { + success: boolean + challengeData: string + message: string + } + + try { + const response: AxiosResponse = await axios.post(APIBaseURL + '/v1/dapp-auth/challenge', requestPayload) + const responseData: Response = response.data + + if (!responseData.success) { + throw new Error('obtainChallenge fail (success=false)') + } + return [responseData.challengeData, responseData.message] + } catch (error) { + throw new Error('obtainChallenge Error -> ' + error.toString()) + } + } + + async function signMessage (message: string): Promise { + const wallet = new ethers.Wallet(process.env.TEST_ENV_KEY) + console.log('sign message with wallet: ', wallet.address) + return await wallet.signMessage(message) + } + + // note: this function generate signature with EIP191 + async function obtainBearerToken (challengeData: string, message: string): Promise { + const signature = await signMessage(message) + + const requestPayload = { + challengeData: challengeData, + signature: signature + } + + interface Response { + success: boolean + bearerToken: string // will last 1 hour + } + + try { + const response: AxiosResponse = await axios.post(APIBaseURL + '/v1/dapp-auth/solve', requestPayload) + const responseData: Response = response.data + + if (!responseData.success) { + throw new Error('obtainBearerToken fail (success=false)') + } + + return responseData.bearerToken + } catch (error) { + throw new Error('obtainBearerToken Error -> ' + error.toString()) + } + } + + async function checkPointBalance (contractAddress: string): Promise { + // GET /v1/contracts/:contractAddress/point-balances + + try { + const pointURL = APIBaseURL + '/v1/contracts/' + contractAddress + '/point-balances' + + const config = { + headers: { Authorization: `Bearer ${bearerToken}` } + } + + const response: AxiosResponse = await axios.get(pointURL, config) + const responseData: PointBalancesResponse = response.data + + if (!responseData.success) { + throw new Error('checkPointBalance fail (success=false)') + } + return responseData + } catch (error) { + throw new Error('checkPointBalance Error -> ' + error.toString()) + } + } + + it('should get blast point', async () => { + const contractAddress = '0x7fc24BaF14D225522242D6E50264E40EDc6bD0DF' + // const contractAddress = '0xF7cCFaee69cD8A0B3a62C2A0f35F95cC7e588183' + const operatorAddress = '0xadBd636A9fF51f2aB6999833AAB784f2C1Efa6F1' + + try { + const [challenge, message] = await obtainChallenge(contractAddress, operatorAddress) + + // set bearerToken in this block global variable + bearerToken = await obtainBearerToken(challenge, message) + + const pointBalances = await checkPointBalance(contractAddress) + // console.log('pointBalances:', pointBalances) + } catch (error) { + // Handle errors + console.error(error) + expect.fail('should not fail') + } + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 90bd4e5..db51742 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1901,6 +1901,15 @@ axios@^0.21.1, axios@^0.21.2: dependencies: follow-redirects "^1.14.0" +axios@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -4995,6 +5004,11 @@ follow-redirects@^1.12.1, follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + for-each@^0.3.3, for-each@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -8007,6 +8021,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"