diff --git a/.gitmodules b/.gitmodules index 1a48975a..34c72f56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,8 +4,8 @@ branch = releases/v0.7 [submodule "crates/types/contracts/lib/account-abstraction-versions/v0_6"] path = crates/types/contracts/lib/account-abstraction-versions/v0_6 - url = https://github.com/bobanetwork/account-abstraction-hc - branch = hc-dev + url = https://github.com/eth-infinitism/account-abstraction + branch = releases/v0.6 [submodule "crates/types/contracts/lib/forge-std"] path = crates/types/contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std diff --git a/crates/types/contracts/hc_scripts/ExampleDeploy.s.sol b/crates/types/contracts/hc_scripts/ExampleDeploy.s.sol index 2bf652be..9e37649b 100644 --- a/crates/types/contracts/hc_scripts/ExampleDeploy.s.sol +++ b/crates/types/contracts/hc_scripts/ExampleDeploy.s.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/samples/HybridAccount.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/test/TestAuctionSystem.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/test/TestCaptcha.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/test/TestCounter.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/test/TestRainfallInsurance.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/test/TestSportsBetting.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/test/TestKyc.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/test/TestTokenPrice.sol"; +import "src/hc0_6/HybridAccount.sol"; +import "src/hc0_6/TestAuctionSystem.sol"; +import "src/hc0_6/TestCaptcha.sol"; +import "src/hc0_6/TestHybrid.sol"; +import "src/hc0_6/TestRainfallInsurance.sol"; +import "src/hc0_6/TestSportsBetting.sol"; +import "src/hc0_6/TestKyc.sol"; +import "src/hc0_6/TestTokenPrice.sol"; contract LocalDeploy is Script { function run() external @@ -25,7 +25,7 @@ contract LocalDeploy is Script { ret[0] = address(new AuctionFactory(ha1Addr)); ret[1] = address(new TestCaptcha(ha1Addr)); - ret[2] = address(new TestCounter(ha1Addr)); + ret[2] = address(new TestHybrid(ha1Addr)); ret[3] = address(new RainfallInsurance(ha1Addr)); ret[4] = address(new SportsBetting(ha1Addr)); ret[5] = address(new TestKyc(ha1Addr)); diff --git a/crates/types/contracts/hc_scripts/LocalDeploy.s.sol b/crates/types/contracts/hc_scripts/LocalDeploy.s.sol index ae6c9b09..9b6eecaa 100644 --- a/crates/types/contracts/hc_scripts/LocalDeploy.s.sol +++ b/crates/types/contracts/hc_scripts/LocalDeploy.s.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; import "lib/account-abstraction-versions/v0_6/contracts/core/EntryPoint.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/core/HCHelper.sol"; -import "lib/account-abstraction-versions/v0_6/contracts/samples/HybridAccountFactory.sol"; +import "src/hc0_6/HCHelper.sol"; +import "src/hc0_6/HybridAccountFactory.sol"; import "lib/account-abstraction-versions/v0_6/contracts/samples/SimpleAccountFactory.sol"; contract LocalDeploy is Script { diff --git a/crates/types/contracts/lib/account-abstraction-versions/v0_6 b/crates/types/contracts/lib/account-abstraction-versions/v0_6 index c79c0f59..fa61290d 160000 --- a/crates/types/contracts/lib/account-abstraction-versions/v0_6 +++ b/crates/types/contracts/lib/account-abstraction-versions/v0_6 @@ -1 +1 @@ -Subproject commit c79c0f5910d2db18cff494883e38e3c2a9d2a6b1 +Subproject commit fa61290d37d079e928d92d53a122efcc63822214 diff --git a/crates/types/contracts/src/hc0_6/HCHelper.sol b/crates/types/contracts/src/hc0_6/HCHelper.sol new file mode 100644 index 00000000..044c4a14 --- /dev/null +++ b/crates/types/contracts/src/hc0_6/HCHelper.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.12; + +import "account-abstraction/v0_6/interfaces/INonceManager.sol"; +import "openzeppelin-contracts-versions/v4_9/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract HCHelper { + using SafeERC20 for IERC20; + + // Response data is stored here by PutResponse() and then consumed by TryCallOffchain(). + // The storage slot must not be changed unless the corresponding code is updated in the Bundler. + mapping(bytes32=>bytes) ResponseCache; + + // Owner (creator) of this contract. + address public owner; + + // 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; + + // Data stored per RegisteredCaller + struct callerInfo { + address owner; + string url; + uint256 credits; + } + + // Contracts which are allowed to use Hybrid Compute. + mapping(address=>callerInfo) public RegisteredCallers; + + // AA EntryPoint + address immutable entryPoint; + + // Constructor + constructor(address _entryPoint, address _tokenAddr) { + entryPoint = _entryPoint; + tokenAddr = _tokenAddr; + } + + // Initialize system addresses. Note - can be called again to change + // these addresses if necessary. + function initialize(address _owner, address _systemAccount) public { + require(msg.sender == owner || address(0) == owner, "Only owner"); + owner = _owner; + systemAccount = _systemAccount; + } + + // Change the SystemAccount address (used for error responses) + function SetSystemAccount(address _systemAccount) public { + require(msg.sender == owner, "Only owner"); + systemAccount = _systemAccount; + } + + // Temporary method, until an auto-registration protocol is developed. + function RegisterUrl(address contract_addr, string calldata url) public { + require(msg.sender == owner, "Only owner"); + RegisteredCallers[contract_addr].owner = msg.sender; + RegisteredCallers[contract_addr].url = 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 { + require(msg.sender == owner, "Only owner"); + pricePerCall = _pricePerCall; + } + + // 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 { + if (pricePerCall > 0) { + uint256 tokenPrice = numCredits * pricePerCall; + IERC20(tokenAddr).safeTransferFrom(msg.sender, address(this), tokenPrice); + } + RegisteredCallers[contract_addr].credits += numCredits; + } + + // Allow the owner to withdraw tokens + function WithdrawTokens(uint256 amount, address withdrawTo) public { + require(msg.sender == owner, "Only owner"); + IERC20(tokenAddr).safeTransferFrom(address(this), withdrawTo, amount); + } + + // Called from a HybridAccount contract, to populate the response which it will + // subsequently request in TryCallOffchain() + function PutResponse(bytes32 subKey, bytes calldata response) public { + require(RegisteredCallers[msg.sender].owner != address(0), "Unregistered caller"); + require(response.length >= 32*4, "Response too short"); + + (,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes)); + require(errCode < 2, "invalid errCode for PutResponse()"); + + bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey)); + ResponseCache[mapKey] = response; + } + + // Allow the system to supply an error response for unsuccessful requests. + // Any such response will only be retrieved if there was nothing supplied + // by PutResponse() + function PutSysResponse(bytes32 subKey, bytes calldata response) public { + require(msg.sender == systemAccount, "Only systemAccount may call PutSysResponse"); + require(response.length >= 32*4, "Response too short"); + + (,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes)); + require(errCode >= 2, "PutSysResponse() may only be used for error responses"); + + bytes32 mapKey = keccak256(abi.encodePacked(address(this), subKey)); + ResponseCache[mapKey] = response; + } + + // Remove one or more map entries (only needed if response was not retrieved normally). + function RemoveResponses(bytes32[] calldata mapKeys) public { + require(msg.sender == systemAccount, "Only systemAccount may call RemoveResponses"); + for (uint32 i = 0; i < mapKeys.length; i++) { + delete(ResponseCache[mapKeys[i]]); + } + } + + // Try to retrieve an entry, also removing it from the mapping. This + // function will check for stale entries by checking the nonce of the srcAccount. + // Stale entries will return a "not found" condition. + function getEntry(bytes32 mapKey) internal returns (bool, uint32, bytes memory) { + bytes memory entry; + bool found; + uint32 errCode; + bytes memory response; + address srcAddr; + uint256 srcNonce; + + entry = ResponseCache[mapKey]; + if (entry.length == 1) { + // Used during state simulation to verify that a trigger request actually came from this helper contract + revert("_HC_VRFY"); + } else if (entry.length != 0) { + found = true; + (srcAddr, srcNonce, errCode, response) = abi.decode(entry,(address, uint256, uint32, bytes)); + uint192 nonceKey = uint192(srcNonce >> 64); + + INonceManager NM = INonceManager(entryPoint); + uint256 actualNonce = NM.getNonce(srcAddr, nonceKey); + + if (srcNonce + 1 != actualNonce) { + // stale entry + found = false; + errCode = 0; + response = "0x"; + } + + delete(ResponseCache[mapKey]); + } + return (found, errCode, response); + } + + // Make an offchain call to a pre-registered endpoint. + function TryCallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) { + bool found; + uint32 errCode; + bytes memory ret; + + require(RegisteredCallers[msg.sender].owner != address(0), "Calling contract not registered"); + + if (RegisteredCallers[msg.sender].credits == 0) { + return (5, "Insufficient credit"); + } + RegisteredCallers[msg.sender].credits -= 1; + + bytes32 subKey = keccak256(abi.encodePacked(userKey, req)); + bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey)); + + (found, errCode, ret) = getEntry(mapKey); + + if (found) { + return (errCode, ret); + } else { + // If no off-chain response, check for a system error response. + bytes32 errKey = keccak256(abi.encodePacked(address(this), subKey)); + + (found, errCode, ret) = getEntry(errKey); + if (found) { + require(errCode >= 2, "invalid errCode"); + return (errCode, ret); + } else { + // Nothing found, so trigger a new request. + bytes memory prefix = "_HC_TRIG"; + bytes memory r2 = bytes.concat(prefix, abi.encodePacked(msg.sender, userKey, req)); + assembly { + revert(add(r2, 32), mload(r2)) + } + } + } + } +} diff --git a/crates/types/contracts/src/hc0_6/HybridAccount.sol b/crates/types/contracts/src/hc0_6/HybridAccount.sol new file mode 100644 index 00000000..8295555b --- /dev/null +++ b/crates/types/contracts/src/hc0_6/HybridAccount.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +import "openzeppelin-contracts-versions/v4_9/contracts/utils/cryptography/ECDSA.sol"; +import "openzeppelin-contracts-versions/v4_9/contracts/proxy/utils/Initializable.sol"; +import "openzeppelin-contracts-versions/v4_9/contracts/proxy/utils/UUPSUpgradeable.sol"; + +import "account-abstraction/v0_6/core/BaseAccount.sol"; +import "account-abstraction/v0_6/samples/callback/TokenCallbackHandler.sol"; + +interface IHCHelper { + function TryCallOffchain(bytes32, bytes memory) external returns (uint32, bytes memory); +} +/** + * minimal account. + * this is sample minimal account. + * has execute, eth handling methods + * has a single signer that can send requests through the entryPoint. + */ +contract HybridAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable { + using ECDSA for bytes32; + + mapping(address=>bool) public PermittedCallers; + + address public owner; + + IEntryPoint private immutable _entryPoint; + address public immutable _helperAddr; + + event HybridAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner); + + modifier onlyOwner() { + _onlyOwner(); + _; + } + + /// @inheritdoc BaseAccount + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + constructor(IEntryPoint anEntryPoint, address helperAddr) { + _entryPoint = anEntryPoint; + _helperAddr = helperAddr; + _disableInitializers(); + } + + function _onlyOwner() internal view { + //directly from EOA owner, or through the account itself (which gets redirected through execute()) + require(msg.sender == owner || msg.sender == address(this), "only owner"); + } + + /** + * execute a transaction (called directly from owner, or by entryPoint) + */ + function execute(address dest, uint256 value, bytes calldata func) external { + _requireFromEntryPointOrOwner(); + _call(dest, value, func); + } + + /** + * execute a sequence of transactions + */ + function executeBatch(address[] calldata dest, bytes[] calldata func) external { + _requireFromEntryPointOrOwner(); + require(dest.length == func.length, "wrong array lengths"); + for (uint256 i = 0; i < dest.length; i++) { + _call(dest[i], 0, func[i]); + } + } + + /** + * @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint, + * a new implementation of HybridAccount must be deployed with the new EntryPoint address, then upgrading + * the implementation by calling `upgradeTo()` + */ + function initialize(address anOwner) public virtual initializer { + _initialize(anOwner); + } + + function _initialize(address anOwner) internal virtual { + owner = anOwner; + emit HybridAccountInitialized(_entryPoint, owner); + } + + // Require the function call went through EntryPoint or owner + function _requireFromEntryPointOrOwner() internal view { + require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint"); + } + + /// implement template method of BaseAccount + function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal override virtual returns (uint256 validationData) { + bytes32 hash = userOpHash.toEthSignedMessageHash(); + if (owner != hash.recover(userOp.signature)) + return SIG_VALIDATION_FAILED; + return 0; + } + + function _call(address target, uint256 value, bytes memory data) internal { + (bool success, bytes memory result) = target.call{value : value}(data); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + /** + * check current account deposit in the entryPoint + */ + function getDeposit() public view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /** + * deposit more funds for this account in the entryPoint + */ + function addDeposit() public payable { + entryPoint().depositTo{value : msg.value}(address(this)); + } + + /** + * withdraw value from the account's deposit + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint().withdrawTo(withdrawAddress, amount); + } + + function _authorizeUpgrade(address newImplementation) internal view override { + (newImplementation); + _onlyOwner(); + } + + function PermitCaller(address caller, bool allowed) public { + _requireFromEntryPointOrOwner(); + PermittedCallers[caller] = allowed; + } + + function CallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) { + /* By default a simple whitelist is used. Endpoint implementations may choose to allow + unrestricted access, to use a custom permission model, to charge fees, etc. */ + require(_helperAddr != address(0), "Helper address not set"); + require(PermittedCallers[msg.sender], "Permission denied"); + IHCHelper HC = IHCHelper(_helperAddr); + + userKey = keccak256(abi.encodePacked(userKey, msg.sender)); + return HC.TryCallOffchain(userKey, req); + } +} diff --git a/crates/types/contracts/src/hc0_6/HybridAccountFactory.sol b/crates/types/contracts/src/hc0_6/HybridAccountFactory.sol new file mode 100644 index 00000000..5a9bfd1f --- /dev/null +++ b/crates/types/contracts/src/hc0_6/HybridAccountFactory.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "openzeppelin-contracts-versions/v4_9/contracts/utils/Create2.sol"; +import "openzeppelin-contracts-versions/v4_9/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import "./HybridAccount.sol"; + +/** + * A sample factory contract for HybridAccount + * A UserOperations "initCode" holds the address of the factory, and a method call (to createAccount, in this sample factory). + * The factory's createAccount returns the target account address even if it is already installed. + * This way, the entryPoint.getSenderAddress() can be called either before or after the account is created. + */ +contract HybridAccountFactory { + HybridAccount public immutable accountImplementation; + address public Helper; + + constructor(IEntryPoint _entryPoint, address _helper) { + accountImplementation = new HybridAccount(_entryPoint, _helper); + Helper = _helper; + } + + /** + * create an account, and return its address. + * returns the address even if the account is already deployed. + * Note that during UserOperation execution, this method is called only if the account is not deployed. + * This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation + */ + function createAccount(address owner,uint256 salt) public returns (HybridAccount ret) { + address addr = getAddress(owner, salt); + uint codeSize = addr.code.length; + if (codeSize > 0) { + return HybridAccount(payable(addr)); + } + ret = HybridAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}( + address(accountImplementation), + abi.encodeCall(HybridAccount.initialize, (owner)) + ))); + } + + /** + * calculate the counterfactual address of this account as it would be returned by createAccount() + */ + function getAddress(address owner,uint256 salt) public view returns (address) { + return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode( + address(accountImplementation), + abi.encodeCall(HybridAccount.initialize, (owner)) + ) + ))); + } +} diff --git a/crates/types/contracts/src/hc0_6/TestAuctionSystem.sol b/crates/types/contracts/src/hc0_6/TestAuctionSystem.sol new file mode 100644 index 00000000..15a446f3 --- /dev/null +++ b/crates/types/contracts/src/hc0_6/TestAuctionSystem.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./HybridAccount.sol"; + +contract AuctionFactory { + uint256 public auctionCount = 0; + mapping(uint256 => Auction) public auctions; + + event AuctionCreated(uint256 auctionId, address auctionAddress); + event AuctionEnded(uint256 auctionId, address winner, uint256 amount); + + struct Auction { + address highestBidder; + uint256 highestBid; + uint256 endTime; + address payable beneficiary; + bool ended; + } + + address payable immutable helperAddr; + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + } + + modifier auctionExists(uint256 auctionId) { + require(auctions[auctionId].beneficiary != address(0), "Auction does not exist"); + _; + } + + function createAuction(uint256 _biddingTime, address payable _beneficiary) public { + auctionCount++; + auctions[auctionCount] = Auction({ + highestBidder: address(0), + highestBid: 0, + endTime: block.timestamp + _biddingTime, + beneficiary: _beneficiary, + ended: false + }); + emit AuctionCreated(auctionCount, address(this)); + } + + function bid(uint256 auctionId) public payable auctionExists(auctionId) { + Auction storage auction = auctions[auctionId]; + require(block.timestamp < auction.endTime, "Auction already ended."); + require(msg.value > auction.highestBid, "There already is a higher bid."); + require(verifyBidder(), "Bidder not verified."); + + if (auction.highestBidder != address(0)) { + payable(auction.highestBidder).transfer(auction.highestBid); + } + + auction.highestBidder = msg.sender; + auction.highestBid = msg.value; + } + + function verifyBidder() private returns (bool) { + HybridAccount ha = HybridAccount(helperAddr); + + bytes memory req = abi.encodeWithSignature( + "verifyBidder(address)", + msg.sender + ); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + bool isVerified; + (isVerified) = abi.decode(ret, (bool)); + return isVerified; + } + + function endAuction(uint256 auctionId) public auctionExists(auctionId) { + Auction storage auction = auctions[auctionId]; + require(block.timestamp >= auction.endTime, "Auction not yet ended."); + require(!auction.ended, "Auction end already called."); + + auction.ended = true; + emit AuctionEnded(auctionId, auction.highestBidder, auction.highestBid); + + auction.beneficiary.transfer(auction.highestBid); + } + + function getHighestBid(uint256 auctionId) public view auctionExists(auctionId) returns (uint256) { + return auctions[auctionId].highestBid; + } + + function getHighestBidder(uint256 auctionId) public view auctionExists(auctionId) returns (address) { + return auctions[auctionId].highestBidder; + } + + function getAuctionEndTime(uint256 auctionId) public view auctionExists(auctionId) returns (uint256) { + return auctions[auctionId].endTime; + } + + function isAuctionEnded(uint256 auctionId) public view auctionExists(auctionId) returns (bool) { + return auctions[auctionId].ended; + } +} diff --git a/crates/types/contracts/src/hc0_6/TestCaptcha.sol b/crates/types/contracts/src/hc0_6/TestCaptcha.sol new file mode 100644 index 00000000..ccde4ec3 --- /dev/null +++ b/crates/types/contracts/src/hc0_6/TestCaptcha.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./HybridAccount.sol"; + +contract TestCaptcha is Ownable { + address payable immutable helperAddr; + uint256 constant public nativeFaucetAmount = 0.01 ether; + uint256 constant public waitingPeriod = 1 days; + IERC20 public token; + + mapping(address => uint256) public claimRecords; + + uint256 private constant SAFE_GAS_STIPEND = 6000; + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + } + + event Withdraw(address receiver, uint256 nativeAmount); + + receive() external payable {} + + function withdraw(uint256 _nativeAmount) public onlyOwner { + (bool sent, ) = msg.sender.call{ + gas: SAFE_GAS_STIPEND, + value: _nativeAmount + }(""); + require(sent, "Failed to send native Ether"); + + emit Withdraw(msg.sender, _nativeAmount); + } + + function verifyCaptcha( + address _to, + bytes32 _uuid, + string memory _key + ) private returns (bool) { + HybridAccount ha = HybridAccount(helperAddr); + + bytes memory req = abi.encodeWithSignature( + "verifyCaptcha(string,string,string)", + _to, + _uuid, + _key + ); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + bool isVerified; + (isVerified) = abi.decode(ret, (bool)); + return isVerified; + } + + function getTestnetETH( + bytes32 _uuid, + string memory _key, + address _to) external { + require(claimRecords[_to] + waitingPeriod <= block.timestamp, 'Invalid request'); + require(verifyCaptcha(_to, _uuid, _key), "Invalid captcha"); + claimRecords[_to] = block.timestamp; + + (bool sent,) = (_to).call{gas: SAFE_GAS_STIPEND, value: nativeFaucetAmount}(""); + require(sent, "Failed to send native"); + } +} diff --git a/crates/types/contracts/src/hc0_6/TestHybrid.sol b/crates/types/contracts/src/hc0_6/TestHybrid.sol new file mode 100644 index 00000000..f25ba594 --- /dev/null +++ b/crates/types/contracts/src/hc0_6/TestHybrid.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +//sample "receiver" contract, for testing "exec" from account. + +//interface IHybridAccount { +// function CallOffchain(bytes32, bytes memory) external returns (uint32, bytes memory); +//} +import "./HybridAccount.sol"; + +contract TestHybrid { + mapping(address => uint256) public counters; + + address payable immutable demoAddr; + + constructor(address payable _demoAddr) { + demoAddr = _demoAddr; + } + + function count(uint32 a, uint32 b) public { + HybridAccount HA = HybridAccount(demoAddr); + uint256 x; + uint256 y; + if (b == 0) { + counters[msg.sender] = counters[msg.sender] + a; + return; + } + bytes memory req = abi.encodeWithSignature("addsub2(uint32,uint32)", a, b); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = HA.CallOffchain(userKey, req); + + if (error == 0) { + (x,y) = abi.decode(ret,(uint256,uint256)); // x=(a+b), y=(a-b) + + this.gasWaster(x, "abcd1234"); + counters[msg.sender] = counters[msg.sender] + y; + } else if (b >= 10) { + revert(string(ret)); + } else if (error == 1) { + counters[msg.sender] = counters[msg.sender] + 100; + } else { + //revert(string(ret)); + counters[msg.sender] = counters[msg.sender] + 1000; + } + + } + + function countFail() public pure { + revert("count failed"); + } + + function justemit() public { + emit CalledFrom(msg.sender); + } + + event CalledFrom(address sender); + + //helper method to waste gas + // repeat - waste gas on writing storage in a loop + // junk - dynamic buffer to stress the function size. + mapping(uint256 => uint256) public xxx; + uint256 public offset; + + function gasWaster(uint256 repeat, string calldata /*junk*/) external { + for (uint256 i = 1; i <= repeat; i++) { + offset++; + xxx[offset] = i; + } + } + + /* This example is a word-guessing game. The user picks a four-letter word as their guess, + and pays for the number of entries they wish to purchase. This wager is added to a pool. + The offchain provider generates a random array of words and returns it as a string[]. If + the user's guess appears in the list returned from the server then they win the entire pool. + + A boolean flag allows the user to cheat by guaranteeing that the word "frog" will appear + in the list. + */ + + event GameResult(address indexed caller,uint256 indexed win, uint256 indexed Pool); + uint256 public constant EntryCost = 2 gwei; + uint256 public Pool = 0; + + function wordGuess(string calldata myGuess, bool cheat) public payable { + HybridAccount HA = HybridAccount(payable(demoAddr)); + uint256 entries = msg.value / EntryCost; + require(entries > 0, "No entries purchased"); + require(entries <= 100, "Excess payment"); + Pool += msg.value; + require(bytes(myGuess).length == 4, "Game uses 4-letter words"); + + bytes memory req = abi.encodeWithSignature("ramble(uint256,bool)", entries, cheat); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = HA.CallOffchain(userKey, req); + if (error != 0) { + revert(string(ret)); + } + + uint256 win = 0; + string[] memory words = abi.decode(ret,(string[])); + + for (uint256 i=0; i Policy) public policies; + mapping(string => Rainfall) public currentRainfall; + uint256 public constant MULTIPLIER = 3; + address payable immutable helperAddr; + + uint256 private nonce; + + event PolicyCreated(uint256 indexed policyId, address indexed policyholder, string city, uint256 premium, uint256 payoutAmount); + + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + } + + function generatePolicyId(address policyHolder, string memory city) internal returns (uint256) { + nonce++; + return uint256(keccak256(abi.encodePacked(policyHolder, city, block.timestamp, nonce))); + } + + function buyInsurance( + uint256 triggerRainfall, + string memory city + ) public payable returns (uint256){ + require(msg.value > 0, "Premium must be greater than zero"); + uint256 payoutAmount = msg.value * MULTIPLIER; + uint256 policyId = generatePolicyId(msg.sender, city); + + policies[policyId] = Policy( + msg.sender, + msg.value, + payoutAmount, + triggerRainfall, + city, + block.timestamp, + PolicyState.Active + ); + + emit PolicyCreated(policyId, msg.sender, city, msg.value, payoutAmount); + } + + function updateRainfall( + string memory city + ) internal returns (Rainfall storage) { + HybridAccount ha = HybridAccount(helperAddr); + + bytes memory req = abi.encodeWithSignature( + "get_rainfall(string)", + city + ); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + uint256 rainfallInMm; + (rainfallInMm) = abi.decode(ret, (uint256)); + currentRainfall[city] = Rainfall(rainfallInMm, block.timestamp); + + return currentRainfall[city]; + } + + function checkAndPayout(uint256 policyId) public { + Policy storage policy = policies[policyId]; + require(policy.state == PolicyState.Active, "Policy is not active"); + + if (policy.timestamp + 365 days < block.timestamp) { + policy.state = PolicyState.Expired; + revert("Policy expired"); + } + + Rainfall storage rainfall = currentRainfall[policy.city]; + + if ( + rainfall.updatedAt == 0 || + rainfall.updatedAt + 24 hours < block.timestamp + ) { + rainfall = updateRainfall(policy.city); + } + + + require( + rainfall.rainfallInMm <= policy.triggerRainfall, + "Trigger condition not met" + ); + + policy.state = PolicyState.Claimed; + payable(policy.policyholder).transfer(policy.payoutAmount); + } +} diff --git a/crates/types/contracts/src/hc0_6/TestSportsBetting.sol b/crates/types/contracts/src/hc0_6/TestSportsBetting.sol new file mode 100644 index 00000000..a515e6c9 --- /dev/null +++ b/crates/types/contracts/src/hc0_6/TestSportsBetting.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./HybridAccount.sol"; + +contract SportsBetting { + address payable immutable helperAddr; + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + } + + struct Bet { + address bettor; + uint256 amount; + uint256 outcome; // 1 for Team A win, 2 for Team B win, 3 for Draw + bool settled; + } + + struct Game { + uint256 gameId; + bool exists; + } + + mapping(uint256 => Game) public games; + mapping(uint256 => Bet[]) public bets; + mapping(uint256 => uint256) public gameScores; // 1 for Team A win, 2 for Team B win, 3 for Draw + + event GameCreated(uint256 indexed gameId); + event BetPlaced( + address indexed bettor, + uint256 indexed gameId, + uint256 amount, + uint256 outcome + ); + event BetSettled( + address indexed bettor, + uint256 indexed gameId, + uint256 outcome, + uint256 winnings + ); + event GameScoreUpdated(uint256 indexed gameId, uint256 score); + + function createGame(uint256 gameId) external returns (uint256) { + games[gameId] = Game({gameId: gameId, exists: true}); + + emit GameCreated(gameId); + return gameId; + } + + function placeBet(uint256 _gameId, uint256 _outcome) external payable { + require(msg.value > 0, "Bet amount must be greater than zero"); + require(_outcome >= 1 && _outcome <= 3, "Invalid outcome"); + require(games[_gameId].exists, "Game does not exist"); + + bets[_gameId].push( + Bet({ + bettor: msg.sender, + amount: msg.value, + outcome: _outcome, + settled: false + }) + ); + + emit BetPlaced(msg.sender, _gameId, msg.value, _outcome); + } + + function settleBet(uint256 _gameId) external { + require(games[_gameId].exists, "Game does not exist"); + + uint256 actualOutcome = updateGameScore(_gameId); + //uint256 actualOutcome = gameScores[_gameId]; + + for (uint256 i = 0; i < bets[_gameId].length; i++) { + Bet storage bet = bets[_gameId][i]; + if (!bet.settled) { + if (bet.outcome == actualOutcome) { + uint256 winnings = bet.amount * 2; // Here you could fetch the winning ratio from offchain to calculate the user's win. + payable(bet.bettor).transfer(winnings); + emit BetSettled( + bet.bettor, + _gameId, + actualOutcome, + winnings + ); + } + bet.settled = true; + } + } + } + + function updateGameScore(uint256 _gameId) internal returns (uint256) { + require(games[_gameId].exists, "Game does not exist"); + + HybridAccount ha = HybridAccount(helperAddr); + + bytes memory req = abi.encodeWithSignature( + "get_score(uint256)", + _gameId + ); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + uint256 result; + (result) = abi.decode(ret, (uint256)); + gameScores[_gameId] = result; + emit GameScoreUpdated(_gameId, result); + return result; + } +} diff --git a/crates/types/contracts/src/hc0_6/TestTokenPrice.sol b/crates/types/contracts/src/hc0_6/TestTokenPrice.sol new file mode 100644 index 00000000..9dcbf397 --- /dev/null +++ b/crates/types/contracts/src/hc0_6/TestTokenPrice.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./HybridAccount.sol"; + +contract TestTokenPrice { + mapping(uint256 => uint256) public counters; + address payable immutable helperAddr; + + event PriceQuote(string, string); + + constructor(address payable _helperAddr) { + helperAddr = _helperAddr; + counters[0] = 100; + } + + function fetchPrice( + string calldata token + ) public returns (string memory) { + HybridAccount ha = HybridAccount(payable(helperAddr)); + string memory price; + + bytes memory req = abi.encodeWithSignature("getprice(string)", token); + bytes32 userKey = bytes32(abi.encode(msg.sender)); + (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req); + + if (error != 0) { + revert(string(ret)); + } + + (price) = abi.decode(ret, (string)); + emit PriceQuote(token, price); + return price; + } +} diff --git a/crates/types/contracts/src/v0_6/imports.sol b/crates/types/contracts/src/v0_6/imports.sol index 49c96875..0782f7b9 100644 --- a/crates/types/contracts/src/v0_6/imports.sol +++ b/crates/types/contracts/src/v0_6/imports.sol @@ -9,4 +9,4 @@ import "account-abstraction/v0_6/samples/VerifyingPaymaster.sol"; import "account-abstraction/v0_6/interfaces/IEntryPoint.sol"; import "account-abstraction/v0_6/interfaces/IAggregator.sol"; import "account-abstraction/v0_6/interfaces/IStakeManager.sol"; -import "account-abstraction/v0_6/core/HCHelper.sol"; +import "src/hc0_6/HCHelper.sol"; diff --git a/crates/types/src/timestamp.rs b/crates/types/src/timestamp.rs index 5e5187d2..dc75a493 100644 --- a/crates/types/src/timestamp.rs +++ b/crates/types/src/timestamp.rs @@ -266,7 +266,7 @@ mod test { #[test] fn test_out_of_bounds_display() { let actual = get_timestamp_out_of_bounds_for_datetime().to_string(); - assert_eq!(actual, "later than +262143-12-31 23:59:59.999999999 UTC"); + assert_eq!(actual, "later than +262142-12-31 23:59:59.999999999 UTC"); } #[test] diff --git a/deny.toml b/deny.toml index b0069153..72c14ef3 100644 --- a/deny.toml +++ b/deny.toml @@ -2,12 +2,17 @@ # More documentation for the advisories section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] -vulnerability = "deny" -unmaintained = "warn" -unsound = "warn" +#vulnerability = "deny" +#unmaintained = "warn" +#unsound = "warn" yanked = "warn" -notice = "warn" - +#notice = "warn" +ignore = [ + {id = "RUSTSEC-2021-0141", reason = "Unmaintained dependency inherited from upstream"}, + {id = "RUSTSEC-2022-0071", reason = "Unmaintained dependency inherited from upstream"}, + {id = "RUSTSEC-2024-0320", reason = "Unmaintained dependency inherited from upstream"}, + {id = "RUSTSEC-2024-0336", reason = "Dependency inherited from upstream"}, +] # This section is considered when running `cargo deny check bans`. # More documentation about the 'bans' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html @@ -33,9 +38,9 @@ skip = [] skip-tree = [] [licenses] -unlicensed = "deny" +#unlicensed = "deny" confidence-threshold = 0.9 -copyleft = "deny" +#copyleft = "deny" # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses @@ -58,6 +63,7 @@ exceptions = [ { allow = ["CC0-1.0"], name = "tiny-keccak" }, { allow = ["Unicode-DFS-2016"], name = "unicode-ident" }, { allow = ["OpenSSL"], name = "ring" }, + { allow = ["OpenSSL"], name="aws-lc-sys" }, # Inherited dependency via rustls ] diff --git a/hybrid-compute/Dockerfile.offchain-rpc b/hybrid-compute/Dockerfile.offchain-rpc index 26a4f5ef..60bca02e 100644 --- a/hybrid-compute/Dockerfile.offchain-rpc +++ b/hybrid-compute/Dockerfile.offchain-rpc @@ -1,7 +1,7 @@ FROM python:3.8-slim RUN apt update RUN apt install -y wamerican git -RUN pip3 install --default-timeout=100 web3 git+https://github.com/bobanetwork/jsonrpclib.git jsonrpcclient redis python-dotenv +RUN pip3 install --default-timeout=100 web3~=6.14 git+https://github.com/bobanetwork/jsonrpclib.git jsonrpcclient redis python-dotenv COPY ./offchain / COPY ./aa_utils /aa_utils CMD [ "python", "-u", "./offchain.py" ] diff --git a/hybrid-compute/deploy-local.py b/hybrid-compute/deploy-local.py index d09b1f76..26b53e16 100644 --- a/hybrid-compute/deploy-local.py +++ b/hybrid-compute/deploy-local.py @@ -7,7 +7,6 @@ import argparse from web3 import Web3 from web3.middleware import geth_poa_middleware -import solcx from eth_abi import abi as ethabi from aa_utils import * @@ -66,35 +65,27 @@ l1_util = eth_utils(l1) l2_util = eth_utils(w3) -solcx.install_solc("0.8.17") -solcx.set_solc_version("0.8.17") contract_info = {} -PATH_PREFIX = "../crates/types/contracts/lib/account-abstraction-versions/v0_6/contracts/" - -def load_contract(w, name, files): - """Compiles a contract from source and loads its ABI""" - compiled = solcx.compile_files( - files, - output_values=['abi', 'bin', 'bin-runtime'], - import_remappings={ - "@openzeppelin": "../crates/types/contracts/lib/openzeppelin-contracts-versions/v4_9"}, - allow_paths=[PATH_PREFIX], - optimize=True, - optimize_runs=1000000, - ) - - for k in compiled.keys(): - if re.search(re.compile(name), k): - break +OUT_PREFIX = "../crates/types/contracts/out/" + +def load_contract(w, name, path, address): + """Loads a contract's JSON ABI""" + with open(path, "r") as f: + j = json.loads(f.read()) + contract_info[name] = {} - contract_info[name]['abi'] = compiled[k]['abi'] - contract_info[name]['bin'] = compiled[k]['bin'] - return w.eth.contract(abi=contract_info[name]['abi'], bytecode=contract_info[name]['bin']) + contract_info[name]['abi'] = j['abi'] + + deployed[name] = {} + deployed[name]['abi'] = contract_info[name]['abi'] + deployed[name]['address'] = address + + return w.eth.contract(abi=contract_info[name]['abi'], address=address) def submit_as_op(addr, calldata, signer_key): - """Wrapper to build and submit a UserOperation directly to the EntryPoint. We don't + """Wrapper to build and submit a UserOperation directly to the int. We don't have a Bundler to run gas estimation so the values are hard-coded. It might be necessary to change these values e.g. if simulating different L1 prices on the local devnet""" op = { @@ -208,7 +199,7 @@ def deploy_forge(script, cmd_env): args = ["/home/enya/.foundry/bin/forge", "script", "--silent", "--json", "--broadcast"] args.append("--rpc-url=http://127.0.0.1:9545") args.append("--contracts") - args.append("lib/account-abstraction-versions/v0_6/contracts/core") + args.append("src/hc0_6") args.append("--remappings") args.append("@openzeppelin/=lib/openzeppelin-contracts-versions/v4_9") args.append(script) @@ -268,20 +259,7 @@ def boba_balance(addr): bal = w3.eth.call({'to':boba_token, 'data':bal_calldata}) return Web3.to_int(bal) -HH = load_contract(w3, "HCHelper", PATH_PREFIX+"core/HCHelper.sol") -KYC = load_contract(w3, "TestKyc", PATH_PREFIX+"test/TestKyc.sol") -TEST_TOKEN_PRICE = load_contract( - w3, "TestTokenPrice", PATH_PREFIX+"test/TestTokenPrice.sol") -TestCaptcha = load_contract( - w3, "TestCaptcha", PATH_PREFIX+"test/TestCaptcha.sol") -TC = load_contract(w3, "TestCounter", PATH_PREFIX+"test/TestCounter.sol") -TestRainfallInsurance = load_contract( - w3, "TestRainfallInsurance", PATH_PREFIX+"test/TestRainfallInsurance.sol") -EP = load_contract(w3, "EntryPoint", PATH_PREFIX+"core/EntryPoint.sol") -SA = load_contract(w3, "SimpleAccount", PATH_PREFIX+"samples/SimpleAccount.sol") -HA = load_contract(w3, "HybridAccount", PATH_PREFIX+"samples/HybridAccount.sol") -TEST_AUCTION = load_contract(w3, "TestAuctionSystem", PATH_PREFIX+"test/TestAuctionSystem.sol") -SPORT_BET = load_contract(w3, "TestSportsBetting", PATH_PREFIX+"test/TestSportsBetting.sol") +EP = load_contract(w3, "EntryPoint", "../crates/types/contracts/lib/account-abstraction-versions/v0_6/deployments/optimism/EntryPoint.json", "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789") assert l1.eth.get_balance(deploy_addr) > Web3.to_wei(1000, 'ether') @@ -329,17 +307,13 @@ def boba_balance(addr): time.sleep(2) print("Continuing") -deployed = {} - fund_addr(env_vars['BUNDLER_ADDR']) (ep_addr, hh_addr, saf_addr, haf_addr, ha0_addr) = deploy_base() aa = aa_rpc(ep_addr, w3, None) -EP = get_contract('EntryPoint',ep_addr) -HH = get_contract('HCHelper',hh_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({ @@ -352,18 +326,19 @@ def boba_balance(addr): ha1_addr = deploy_account(haf_addr, env_vars['OC_OWNER']) fund_addr_ep(EP, ha1_addr) -HA = get_contract('HybridAccount',ha1_addr) -SA = get_contract('SimpleAccount', client_addr) + +HA = load_contract(w3, 'HybridAccount', OUT_PREFIX + "HybridAccount.sol/HybridAccount.json", ha1_addr) +SA = load_contract(w3, 'SimpleAccount', OUT_PREFIX + "SimpleAccount.sol/SimpleAccount.json", client_addr) example_addrs = deploy_examples(ha1_addr) -TEST_AUCTION = get_contract('TestAuctionSystem', example_addrs[0]) -CAPTCHA = get_contract('TestCaptcha', example_addrs[1]) -TC = get_contract('TestCounter', example_addrs[2]) -RAINFALL_INSURANCE = get_contract('TestRainfallInsurance', example_addrs[3]) -TEST_SPORTS_BETTING = get_contract('TestSportsBetting', example_addrs[4]) -KYC = get_contract('TestKyc', example_addrs[5]) -TEST_TOKEN_PRICE = get_contract('TestTokenPrice', example_addrs[6]) +TEST_AUCTION = load_contract(w3, 'TestAuctionSystem', OUT_PREFIX + "TestAuctionSystem.sol/AuctionFactory.json", example_addrs[0]) +CAPTCHA = load_contract(w3, 'TestCaptcha', OUT_PREFIX + "TestCaptcha.sol/TestCaptcha.json", example_addrs[1]) +TC = load_contract(w3, 'TestHybrid', OUT_PREFIX + "TestHybrid.sol/TestHybrid.json", example_addrs[2]) +RAINFALL_INSURANCE = load_contract(w3, 'TestRainfallInsurance', OUT_PREFIX + "TestRainfallInsurance.sol/RainfallInsurance.json", example_addrs[3]) +TEST_SPORTS_BETTING = load_contract(w3, 'TestSportsBetting', OUT_PREFIX + "TestSportsBetting.sol/SportsBetting.json", example_addrs[4]) +KYC = load_contract(w3, 'TestKyc', OUT_PREFIX + "TestKyc.sol/TestKyc.json", example_addrs[5]) +TEST_TOKEN_PRICE = load_contract(w3, 'TestTokenPrice', OUT_PREFIX + "TestTokenPrice.sol/TestTokenPrice.json", example_addrs[6]) for a in example_addrs: permit_caller(HA, a) diff --git a/hybrid-compute/offchain/userop.py b/hybrid-compute/offchain/userop.py index 2a164f25..ee6fae8f 100644 --- a/hybrid-compute/offchain/userop.py +++ b/hybrid-compute/offchain/userop.py @@ -4,7 +4,7 @@ from check_kyc.check_kyc_test import TestKyc from add_sub_2.add_sub_2_test import TestAddSub2 from ramble.ramble_test import TestWordGuess -from verify_captcha.captcha_test import TestCaptcha +#from verify_captcha.captcha_test import TestCaptcha from auction_system.auction_system_test import TestAuction from sports_betting.sports_betting_test import TestSportsBetting from rainfall_insurance.rainfall_insurance_test import test_rainfall_insurance_purchase,test_rainfall_insurance_payout diff --git a/hybrid-compute/offchain/userop_utils.py b/hybrid-compute/offchain/userop_utils.py index a4047391..47f16856 100644 --- a/hybrid-compute/offchain/userop_utils.py +++ b/hybrid-compute/offchain/userop_utils.py @@ -67,7 +67,7 @@ HA = w3.eth.contract(address=deployed['HybridAccount'] ['address'], abi=deployed['HybridAccount']['abi']) TC = w3.eth.contract( - address=deployed['TestCounter']['address'], abi=deployed['TestCounter']['abi']) + address=deployed['TestHybrid']['address'], abi=deployed['TestHybrid']['abi']) KYC = w3.eth.contract( address=deployed['TestKyc']['address'], abi=deployed['TestKyc']['abi']) TFP = w3.eth.contract( diff --git a/hybrid-compute/requirements.txt b/hybrid-compute/requirements.txt new file mode 100644 index 00000000..7125e8e3 --- /dev/null +++ b/hybrid-compute/requirements.txt @@ -0,0 +1,5 @@ +jsonrpcclient +git+https://github.com/bobanetwork/jsonrpclib.git +python-dotenv +redis +web3~=6.14