Skip to content

Commit 3e7f329

Browse files
committed
Initial checkin of AA/HC demo contracts.
Changes to be committed: new file: contracts/core/HCHelper.sol new file: contracts/samples/HybridAccount.sol modified: contracts/samples/SimpleAccount.sol modified: contracts/test/TestCounter.sol
1 parent fa61290 commit 3e7f329

File tree

4 files changed

+382
-3
lines changed

4 files changed

+382
-3
lines changed

contracts/core/HCHelper.sol

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.12;
3+
4+
interface NonceMgr {
5+
function getNonce(address sender, uint192 key) external view returns (uint256 nonce);
6+
}
7+
8+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9+
10+
contract HCHelper {
11+
using SafeERC20 for IERC20;
12+
13+
// Owner (creator) of this contract.
14+
address public owner;
15+
16+
// Response data is stored here by PutResponse() and then consumed by TryCallOffchain().
17+
// The storage slot must not be changed unless the corresponding code is updated in the Bundler.
18+
mapping(bytes32=>bytes) ResponseCache;
19+
20+
// BOBA token address
21+
address public tokenAddr;
22+
23+
// Token amount required to purchase each prepaid credit (may be 0 for testing)
24+
uint256 public pricePerCall;
25+
26+
// Account which is used to insert system error responses. Currently a single
27+
// address but could be extended to a list of authorized accounts if needed.
28+
address public systemAccount;
29+
30+
31+
struct callerInfo {
32+
address owner;
33+
string url;
34+
uint256 credits;
35+
}
36+
37+
// Contracts which are allowed to use Hybrid Compute.
38+
mapping(address=>callerInfo) public RegisteredCallers;
39+
40+
address immutable entryPoint;
41+
42+
constructor(address _entryPoint, address _tokenAddr, uint256 _pricePerCall) {
43+
owner = msg.sender;
44+
entryPoint = _entryPoint;
45+
tokenAddr = _tokenAddr;
46+
pricePerCall = _pricePerCall;
47+
}
48+
49+
function RegisterUrl(address contract_addr, string calldata url) public {
50+
// Temporary method, until an auto-registration protocol is developed
51+
RegisteredCallers[contract_addr].owner = msg.sender;
52+
RegisteredCallers[contract_addr].url = url;
53+
}
54+
55+
function SetSystemAccount(address _systemAccount) public {
56+
require(msg.sender == owner, "Only owner");
57+
systemAccount = _systemAccount;
58+
}
59+
60+
// Placeholder - this will transfer Boba tokens to this contract (or to a beneficiary)
61+
// and add a corresponding number of credits for the specified contract address.
62+
function AddCredit(address contract_addr, uint256 numCredits) public {
63+
if (pricePerCall > 0) {
64+
uint256 tokenPrice = numCredits * pricePerCall;
65+
IERC20(tokenAddr).safeTransferFrom(msg.sender, address(this), tokenPrice);
66+
}
67+
RegisteredCallers[contract_addr].credits += numCredits;
68+
}
69+
70+
function WithdrawTokens(uint256 amount, address withdrawTo) public {
71+
require(msg.sender == owner, "Only owner");
72+
IERC20(tokenAddr).safeTransferFrom(address(this), withdrawTo, amount);
73+
}
74+
75+
// Called from a HybridAccount contract, to populate the response which it will
76+
// subsequently request in TryCallOffchain()
77+
function PutResponse(bytes32 subKey, bytes calldata response) public {
78+
require(RegisteredCallers[msg.sender].owner != address(0), "Unregistered caller");
79+
require(response.length >= 32*4, "Response too short");
80+
81+
(,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes));
82+
require(errCode < 2, "invalid errCode for PutResponse()");
83+
84+
bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey));
85+
ResponseCache[mapKey] = response;
86+
}
87+
88+
// Allow the system to supply an error response for unsuccessful requests.
89+
// Any such response will only be retrieved if there was nothing supplied
90+
// by PutResponse()
91+
function PutSysResponse(bytes32 subKey, bytes calldata response) public {
92+
require(msg.sender == systemAccount, "Only systemAccount may call PutSysResponse");
93+
require(response.length >= 32*4, "Response too short");
94+
95+
(,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes));
96+
require(errCode >= 2, "PutSysResponse() may only be used for error responses");
97+
98+
bytes32 mapKey = keccak256(abi.encodePacked(address(this), subKey));
99+
ResponseCache[mapKey] = response;
100+
}
101+
102+
// Remove one or more map entries (only needed if response was not retrieved normally).
103+
function RemoveResponse(bytes32[] calldata mapKeys) public {
104+
require(msg.sender == owner, "Only owner");
105+
for (uint32 i = 0; i < mapKeys.length; i++) {
106+
delete(ResponseCache[mapKeys[i]]);
107+
}
108+
}
109+
110+
// Try to retrieve an entry, also removing it from the mapping. This
111+
// function will check for stale entries by checking the nonce of the srcAccount.
112+
// Stale entries will return a "not found" condition.
113+
function getEntry(bytes32 mapKey) internal returns (bool, uint32, bytes memory) {
114+
bytes memory entry;
115+
bool found;
116+
uint32 errCode;
117+
bytes memory response;
118+
address srcAddr;
119+
uint256 srcNonce;
120+
121+
entry = ResponseCache[mapKey];
122+
if (entry.length == 1) {
123+
// Used during state simulation to verify that a trigger request actually came from this helper contract
124+
revert("_HC_VRFY");
125+
} else if (entry.length != 0) {
126+
found = true;
127+
(srcAddr, srcNonce, errCode, response) = abi.decode(entry,(address, uint256, uint32, bytes));
128+
uint192 nonceKey = uint192(srcNonce >> 64);
129+
130+
NonceMgr NM = NonceMgr(entryPoint);
131+
uint256 actualNonce = NM.getNonce(srcAddr, nonceKey);
132+
133+
if (srcNonce + 1 != actualNonce) {
134+
// stale entry
135+
found = false;
136+
errCode = 0;
137+
response = "0x";
138+
}
139+
140+
delete(ResponseCache[mapKey]);
141+
}
142+
return (found, errCode, response);
143+
}
144+
145+
// Make an offchain call to a pre-registered endpoint.
146+
function TryCallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) {
147+
bool found;
148+
uint32 errCode;
149+
bytes memory ret;
150+
151+
require(RegisteredCallers[msg.sender].owner != address(0), "Calling contract not registered");
152+
153+
if (RegisteredCallers[msg.sender].credits == 0) {
154+
return (5, "Insufficient credit");
155+
}
156+
RegisteredCallers[msg.sender].credits -= 1;
157+
158+
bytes32 subKey = keccak256(abi.encodePacked(userKey, req));
159+
bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey));
160+
161+
(found, errCode, ret) = getEntry(mapKey);
162+
163+
if (found) {
164+
return (errCode, ret);
165+
} else {
166+
// If no off-chain response, check for a system error response.
167+
bytes32 errKey = keccak256(abi.encodePacked(address(this), subKey));
168+
169+
(found, errCode, ret) = getEntry(errKey);
170+
if (found) {
171+
require(errCode >= 2, "invalid errCode");
172+
return (errCode, ret);
173+
} else {
174+
// Nothing found, so trigger a new request.
175+
bytes memory prefix = "_HC_TRIG";
176+
bytes memory r2 = bytes.concat(prefix, abi.encodePacked(msg.sender, userKey, req));
177+
assembly {
178+
revert(add(r2, 32), mload(r2))
179+
}
180+
}
181+
}
182+
}
183+
}

contracts/samples/HybridAccount.sol

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.12;
3+
4+
/* solhint-disable avoid-low-level-calls */
5+
/* solhint-disable no-inline-assembly */
6+
/* solhint-disable reason-string */
7+
8+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
9+
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
10+
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
11+
12+
import "../core/BaseAccount.sol";
13+
import "./callback/TokenCallbackHandler.sol";
14+
15+
interface IHCHelper {
16+
function TryCallOffchain(bytes32, bytes memory) external returns (uint32, bytes memory);
17+
}
18+
/**
19+
* minimal account.
20+
* this is sample minimal account.
21+
* has execute, eth handling methods
22+
* has a single signer that can send requests through the entryPoint.
23+
*/
24+
contract HybridAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable {
25+
using ECDSA for bytes32;
26+
27+
mapping(address=>bool) public PermittedCallers;
28+
29+
address public owner;
30+
31+
IEntryPoint private immutable _entryPoint;
32+
address public _helperAddr;
33+
34+
event HybridAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner);
35+
36+
modifier onlyOwner() {
37+
_onlyOwner();
38+
_;
39+
}
40+
41+
/// @inheritdoc BaseAccount
42+
function entryPoint() public view virtual override returns (IEntryPoint) {
43+
require(2>1, "foo");
44+
return _entryPoint;
45+
}
46+
47+
48+
// solhint-disable-next-line no-empty-blocks
49+
receive() external payable {}
50+
51+
constructor(IEntryPoint anEntryPoint, address helperAddr) {
52+
_entryPoint = anEntryPoint;
53+
_helperAddr = helperAddr;
54+
_disableInitializers();
55+
}
56+
57+
function _onlyOwner() internal view {
58+
//directly from EOA owner, or through the account itself (which gets redirected through execute())
59+
require(msg.sender == owner || msg.sender == address(this), "only owner");
60+
}
61+
62+
/**
63+
* execute a transaction (called directly from owner, or by entryPoint)
64+
*/
65+
function execute(address dest, uint256 value, bytes calldata func) external {
66+
_requireFromEntryPointOrOwner();
67+
_call(dest, value, func);
68+
}
69+
70+
/**
71+
* execute a sequence of transactions
72+
*/
73+
function executeBatch(address[] calldata dest, bytes[] calldata func) external {
74+
_requireFromEntryPointOrOwner();
75+
require(dest.length == func.length, "wrong array lengths");
76+
for (uint256 i = 0; i < dest.length; i++) {
77+
_call(dest[i], 0, func[i]);
78+
}
79+
}
80+
81+
/**
82+
* @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint,
83+
* a new implementation of HybridAccount must be deployed with the new EntryPoint address, then upgrading
84+
* the implementation by calling `upgradeTo()`
85+
*/
86+
function initialize(address anOwner) public virtual /*initializer*/ {
87+
_initialize(anOwner);
88+
}
89+
90+
function _initialize(address anOwner) internal virtual {
91+
owner = anOwner;
92+
emit HybridAccountInitialized(_entryPoint, owner);
93+
}
94+
95+
// Require the function call went through EntryPoint or owner
96+
function _requireFromEntryPointOrOwner() internal view {
97+
require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint");
98+
}
99+
100+
/// implement template method of BaseAccount
101+
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
102+
internal override virtual returns (uint256 validationData) {
103+
bytes32 hash = userOpHash.toEthSignedMessageHash();
104+
if (owner != hash.recover(userOp.signature))
105+
return SIG_VALIDATION_FAILED;
106+
return 0;
107+
}
108+
109+
function _call(address target, uint256 value, bytes memory data) internal {
110+
(bool success, bytes memory result) = target.call{value : value}(data);
111+
if (!success) {
112+
assembly {
113+
revert(add(result, 32), mload(result))
114+
}
115+
}
116+
}
117+
118+
/**
119+
* check current account deposit in the entryPoint
120+
*/
121+
function getDeposit() public view returns (uint256) {
122+
return entryPoint().balanceOf(address(this));
123+
}
124+
125+
/**
126+
* deposit more funds for this account in the entryPoint
127+
*/
128+
function addDeposit() public payable {
129+
entryPoint().depositTo{value : msg.value}(address(this));
130+
}
131+
132+
/**
133+
* withdraw value from the account's deposit
134+
* @param withdrawAddress target to send to
135+
* @param amount to withdraw
136+
*/
137+
function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner {
138+
entryPoint().withdrawTo(withdrawAddress, amount);
139+
}
140+
141+
function _authorizeUpgrade(address newImplementation) internal view override {
142+
(newImplementation);
143+
_onlyOwner();
144+
}
145+
146+
function PermitCaller(address caller, bool allowed) public {
147+
_requireFromEntryPointOrOwner();
148+
PermittedCallers[caller] = allowed;
149+
}
150+
151+
function CallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) {
152+
/* By default a simple whitelist is used. Endpoint implementations may choose to allow
153+
unrestricted access, to use a custom permission model, to charge fees, etc. */
154+
require(_helperAddr != address(0), "Helper address not set");
155+
require(PermittedCallers[msg.sender], "Permission denied");
156+
IHCHelper HC = IHCHelper(_helperAddr);
157+
158+
userKey = keccak256(abi.encodePacked(userKey, msg.sender));
159+
return HC.TryCallOffchain(userKey, req);
160+
}
161+
}

contracts/samples/SimpleAccount.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, In
7575
* a new implementation of SimpleAccount must be deployed with the new EntryPoint address, then upgrading
7676
* the implementation by calling `upgradeTo()`
7777
*/
78-
function initialize(address anOwner) public virtual initializer {
78+
function initialize(address anOwner) public virtual /*initializer*/ {
7979
_initialize(anOwner);
8080
}
8181

0 commit comments

Comments
 (0)