├── .gitattributes ├── .gitignore ├── .gitmodules ├── README.md ├── contracts ├── ECDSA.sol ├── Wallet.sol └── Whiteboard.sol └── data └── genesis_template.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | 3 | /data/* 4 | !/data/genesis_template.json 5 | 6 | .DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "go-ethereum"] 2 | path = go-ethereum 3 | url = https://github.com/quilt/go-ethereum.git 4 | [submodule "solidity"] 5 | path = solidity 6 | url = https://github.com/quilt/solidity.git 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Note: Since publishing this playground, we have contined to iterate on the account abstraction specifications and have recently published a [simplified EIP](https://github.com/ethereum/EIPs/pull/2938). Our implementation as presented in this repository is not yet fully in line with this proposal. If there is interest in an updated version of the playground, we will work on bringing it back in sync with the new specs. So let us know if that is something you would want to see! 2 | 3 | # Account Abstraction Playground 4 | 5 | The goal of this playground repo is to get you to create your first real-world account abstraction (AA) contract in 3 simple steps. 6 | Account abstraction is the proposed idea of letting contracts validate and pay for their own transactions, without the involvement of EOAs (i.e. private key accounts). 7 | 8 | 9 | This repo is part of the Quilt team's R&D effort on the feasibility of bringing AA to eth1. 10 | At the core of this effort is our fork of go-ethereum that implements a basic version of AA as [outlined by Vitalik](https://ethereum-magicians.org/t/implementing-account-abstraction-as-part-of-eth1-x/4020) earlier this year. 11 | We are currently in the process of collecting metrics and will be writing a comprehensive overview of our work so far and our future goals once that is done. 12 | In the meantime, this repo aims to enable anyone interested to already explore our current AA MVP implementation. 13 | It tracks the latest stable version of our go-ethereum fork and as such is subject to change as we continue development. 14 | Note that at the current time we do not yet have a position on bringing AA to mainnet, but will communicate our assessment of AA feasibility as part of our upcoming writeup. 15 | 16 | The following instructions are written for macOS, but should be similar for most Linux systems. Windows instructions might differ. 17 | 18 | ## Step 1: Clone & Build 19 | 20 | The repo uses git submodules to bundle our forks of [go-ethereum](https://github.com/quilt/go-ethereum) and [solidity](https://github.com/quilt/solidity) with some additional resources to help with quickly spinning up a local AA testnet. 21 | 22 | #### Clone Recursively 23 | 24 | To clone this repo and both submodules in one step, do: 25 | 26 | ```shell 27 | git clone --recurse-submodules https://github.com/quilt/account-abstraction-playground.git 28 | ``` 29 | 30 | All further commands will be relative to this `account-abstraction-playground` base directory. 31 | 32 | #### Build Go-Ethereum 33 | 34 | For building go-ethereum, you need the most recent version of Go. See [here](https://golang.org/doc/install) for Go install instructions. 35 | On macOS, you also need the Xcode Command Line Tools, which you can install via `xcode-select --install`. 36 | 37 | To compile `geth`, do: 38 | 39 | ```shell 40 | cd go-ethereum 41 | make geth 42 | ``` 43 | 44 | You should now have a `geth` executable at `build/bin/geth`. 45 | Throughout this tutorial, every reference to `geth` is to this modified executable that includes AA support. 46 | 47 | #### Build Solidity 48 | 49 | See the [solidity documentation](https://solidity.readthedocs.io/en/v0.6.10/installing-solidity.html#building-from-source) for building prerequisites. 50 | 51 | To compile `solc`, do: 52 | 53 | ```shell 54 | cd solidity 55 | mkdir build 56 | cd build 57 | cmake .. && make solc 58 | ``` 59 | 60 | You should now have a `solc` executable at `solc/solc`. 61 | 62 | If you are running into a `Could NOT find Boost` issue on macOS, try `brew install boost-python`. 63 | 64 | ## Step 2: Create a Local Test Chain 65 | 66 | The next step is to set up a local geth test chain. If you are already familiar with setting up geth testnets, you can skip this section and do the setup on your own. 67 | Otherwise you can follow this simple 3-step process to set up a local Proof-of-Authority (PoA) testnet: 68 | 69 | #### Create a Signer Account 70 | 71 | To create an account that will serve as the signer (PoA equivalent of a miner) for the testnet, do: 72 | 73 | ```shell 74 | go-ethereum/build/bin/geth account new --datadir data 75 | ``` 76 | 77 | This should output the public address of the newly created account. 78 | We will refer to this address as `__SIGNER__`. 79 | 80 | #### Create a Genesis File 81 | 82 | Next you need to create a genesis file at `data/genesis.json` for the new test chain. 83 | You can use the existing `data/genesis_template.json`, replacing the two occurrences of `__SIGNER__` with the address of your signer, in both cases without the leading `0x`. 84 | 85 | #### Initialize & Start the Chain 86 | 87 | For the last step of the test chain setup, do (once again replacing `__SIGNER__`): 88 | 89 | ```shell 90 | go-ethereum/build/bin/geth init --datadir data data/genesis.json 91 | go-ethereum/build/bin/geth --unlock 0x__SIGNER__ --datadir data --mine --http --http.api personal,eth --allow-insecure-unlock --networkid 12345 --nodiscover 92 | ``` 93 | 94 | After entering the signer account password, you should now see the local geth testnet running and producing new blocks every 3 seconds. 95 | 96 | 97 | ## Step 3: Deploy & Use Your First Account Abstraction Contract 98 | 99 | This repo currently contains two example AA contracts. 100 | The first one, `Whiteboard`, is a simple hello world AA contract that lets you write to and read from a virtual whiteboard. 101 | The second one, `Wallet`, is a more interesting smart contract wallet example that uses `ecrecover` to only accept transactions signed by its owner. 102 | 103 | For interacting with the AA test chain we are using `nodejs` with `web3.js` version `1.2.9`. 104 | 105 | ### `Whiteboard` 106 | 107 | The contract file for the `Whiteboard` contract can be found at [`contracts/Whiteboard.sol`](contracts/Whiteboard.sol). 108 | 109 | #### Description 110 | 111 | When you first look at the contract code, you can see a few small differences to normal solidity code: 112 | 113 | - `pragma experimental AccountAbstraction;` tells the compiler to enable the experimental AA support. 114 | - `account contract Whiteboard {...}` signals that `Whiteboard` is an AA contract. 115 | The compiler therefore includes the AA bytecode prefix at the beginning of the contract bytecode. 116 | - `assembly { paygas(gasPrice) }` uses inline assembly and a new `paygas` Yul function to call the new AA `PAYGAS` opcode with the provided gas price. 117 | `PAYGAS` is required in any AA contract execution (even for read-only access) and signals that the contract has decided to pay for the transaction. 118 | If the transaction fails before `PAYGAS`, it is thus considered invalid and cannot be included in the blockchain at all. 119 | If the transaction fails after `PAYGAS`, it is still considered valid and only state changes after `PAYGAS` are reverted. 120 | The gas price provided to `PAYGAS` is used to deduct `gas price * gas limit` as total transaction cost from the contract balance. 121 | As with normal transactions, any unused gas is then refunded at the end of the transaction. 122 | 123 | 124 | The `Whiteboard` contract itself has two variables: 125 | 126 | - `uint256 nonce` is the contract's internal replay protection mechanism and mirrors the protocol-enshrined nonce functionality. 127 | While it is still an open question whether AA would use the protocol-enshrined nonce, the MVP does not do so. 128 | Thus, all AA transactions have a protocol-level nonce of `0` and it is up to the contract to provide a replay protection mechanism. 129 | - `string message` is the "whiteboard content". It can be changed by the contract via the `setMessage(...)` function. 130 | 131 | Note that the MVP implementation does not currently support public variables, as solidity-defined getter functions do not call `PAYGAS` and are thus not AA compliant. 132 | Instead, the contract provides `getNonce()` and `getMessage()` as getters for `nonce` and `message` respectively. 133 | 134 | To "write" a new message to the whiteboard, the contract provides `function setMessage(uint256 txNonce, uint256 gasPrice, string calldata newMessage, bool failAfterPaygas)`. 135 | This function takes four parameters: 136 | 137 | - `txNonce` is the nonce of the transaction. If it is not equal to the current value of `nonce`, execution aborts without ever reaching `PAYGAS`. 138 | In that case the transaction not only fails, but is considered invalid and cannot be included in a block. 139 | If instead the nonce is valid, the contract then increments its internal nonce by one, making this transaction un-replayable in the future. 140 | - `gasPrice` is the gas price the contract is supposed to pay for the transaction. 141 | While under AA the contract could decide this value on its own, it here lets the caller set it. 142 | - `newMessage` is the new message string that replaces the old `message`. 143 | - `failAfterPaygas` is an extra parameter for AA experimentation. 144 | If set to `true`, the contract will throw. 145 | As `PAYGAS` has already been called at that point, the contract has already committed to paying for the transaction. 146 | Thus, the result is a valid, but failed transaction. If the transaction is included in a block, the contract balance will be reduced by the total transaction fee and `nonce` will be incremented by one. 147 | In contrast, any state changes after `PAYGAS`, in this case the message update, are reverted, resulting in an unchanged `message`. 148 | 149 | Finally, the contract also contains an empty `constructor() public payable`, which allows ETH transfer to the contract as part of its deployment. 150 | Given that AA contracts have to pay for their own transaction, transfering some ETH to it is required. 151 | While the AA prefix of the MVP implementation also allows for incoming ETH transfers to a deployed AA contract, sending ETH as part of the initial deployment makes this extra step unnecessary. 152 | 153 | #### Compile Contract 154 | 155 | To compile the `Whiteboard` contract with the forked version of solidity, do: 156 | 157 | ```shell 158 | solidity/build/solc/solc --bin --abi contracts/Whiteboard.sol 159 | ``` 160 | 161 | This should output both the contract bytecode, which we will reference as `__BYTECODE__`, as well as its ABI, which we will reference as `__ABI__`. 162 | 163 | #### Deploy Contract to Local Chain 164 | 165 | To deploy the compiled contract to the local AA test chain, you first have to ensure geth is still running. 166 | If that is not the case, you can start geth back up via 167 | 168 | ```shell 169 | go-ethereum/build/bin/geth --unlock 0x__SIGNER__ --datadir data --mine --http --http.api personal,eth --allow-insecure-unlock --networkid 12345 --nodiscover 170 | ``` 171 | 172 | The interaction with the chain will now happen from inside `nodejs`, where we first import `web3.js` and connect it to geth: 173 | 174 | ```javascript 175 | const Web3 = require('web3'); 176 | let web3 = new Web3('http://localhost:8545'); 177 | let signer = '0x__SIGNER__'; 178 | web3.eth.getBalance(signer).then(console.log); 179 | ``` 180 | 181 | If you replaced `__SIGNER__` with your signer address, you should now see the current balance of your signer account. 182 | 183 | You can now deploy the `Whiteboard` contract: 184 | 185 | ```javascript 186 | let bytecode = '0x__BYTECODE__'; 187 | let abi = __ABI__; // note: no quotes! 188 | let Contract = new web3.eth.Contract(abi); 189 | let contract; 190 | Contract.deploy({data: bytecode}).send({from: signer, value: 10000000}).then(function(contractInstance){contract = contractInstance; console.log(contractInstance);}); 191 | ``` 192 | 193 | After a few seconds, the AA contract should now be deployed and referenced by the `contract` variable. 194 | The deployment itself was a normal Ethereum transaction, paid for by the signer account. 195 | As the signer is also the block producer and thus collects all transaction fees, the only balance change is the `10000000 wei` sent to the contract as part of its deployment. 196 | To inspect the current balances, you can do: 197 | 198 | ```javascript 199 | web3.eth.getBalance(contract._address).then(console.log); 200 | web3.eth.getBalance(signer).then(console.log); 201 | ``` 202 | 203 | #### Send Your First AA Transaction 204 | 205 | Before sending the first AA transaction, you can first use the static getter functions to read the current contract state: 206 | 207 | ```javascript 208 | contract.options.from = '0xffffffffffffffffffffffffffffffffffffffff'; 209 | contract.methods.getNonce().call().then(console.log); 210 | contract.methods.getMessage().call().then(console.log); 211 | ``` 212 | 213 | This should output a currently empty `message` and a `nonce` of `0`. 214 | Note the first line, where the default address for contract interactions is set to the entry point address. 215 | This is required already for static `ethcall` interactions in order to pass the AA bytecode prefix. 216 | 217 | You can now send your first AA transaction: 218 | 219 | ```javascript 220 | contract.methods.setMessage(0, 1, "hello world!", false).send({gasPrice: "0", gasLimit: 100000}).then(console.log); 221 | ``` 222 | 223 | After a few seconds, the transaction - your first ever AA transaction - should make its way into a block. 224 | To analyze the effect of this transaction, we can again inspect the relevant parts of the chain state: 225 | 226 | ```javascript 227 | contract.methods.getNonce().call().then(console.log); 228 | contract.methods.getMessage().call().then(console.log); 229 | web3.eth.getBalance(contract._address).then(console.log); 230 | web3.eth.getBalance(signer).then(console.log); 231 | ``` 232 | 233 | As you should see, the transaction was successfully executed, incrementing the contract `nonce` by one and setting its `message`. 234 | Furthermore, the contract balance is decreased, indicating that the contract did in fact pay for the transaction on its own. 235 | The signer account had no part in this interaction directly (the caller address was the `0xffffffffffffffffffffffffffffffffffffffff` entry point address) and only collected the transaction fee in its role as block producer. 236 | 237 | This concludes the demonstration of the `Whiteboard` contract. 238 | Feel free to play around with the contract some more - this is the Account Abstraction Playground after all - e.g. by sending a transaction with `failAfterPaygas = true`. 239 | For a somewhat more advanced use case, we will next look at the `Wallet` contract. 240 | 241 | ### `Wallet` 242 | 243 | The contract file for the `Wallet` contract can be found at [`contracts/Wallet.sol`](contracts/Wallet.sol). 244 | 245 | #### Description 246 | 247 | The `Wallet` contract contains 2 variables: 248 | 249 | - `uint256 nonce` is the contract's internal replay protection analogous to the one used in the `Whiteboard` example. 250 | - `address owner` is the contract owner and gets set to the account that deploys the contract. 251 | The owner has to sign all outgoing transfers from the wallet. 252 | 253 | The contract inherits ECDSA signature verification logic from [`contracts/ECDSA.sol`](contracts/ECDSA.sol), which is based on the [OpenZeppelin ECDSA library](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol). 254 | The only change from the OppenZeppelin original is the change from a library to a contract that the `Wallet` contract then inherits from. 255 | As signature verification happens before `PAYGAS`, the AA opcode restrictions apply. 256 | For the MVP, these include a ban of the `DELEGATECALL` used for library interactions. 257 | As AA transaction verification must not rely on external state, there are concerns around library contracts changing their code via `SELFDESTRUCT`, thus potentially rendering previously valid AA transactions invalid. 258 | While we do expect future iterations of AA to support `DELEGATECALL` in some form, for the MVP one has to directly inherit from the `ECDSA` contract such that all of its logic gets deployed together with the rest of the `Wallet` contract. 259 | In contrast to library interactions, calling into precompiles via `STATICCALL` is allowed even before `PAYGAS`. 260 | The `ECDSA` makes use of this by internally calling the `ecrecover` precompile for reconstruction of the signer address. 261 | The relevant function provided by the contract is `recover(bytes32 hash, bytes memory signature)`, which takes a message hash and a 65-byte signature and returns the address of the account that signed the message. 262 | 263 | To transfer ETH from the wallet to an external address, the contract provides `function transfer(uint256 txNonce, uint256 gasPrice, address payable to, uint256 amount, bytes calldata signature)`. 264 | This function takes five parameters: 265 | 266 | - `txNonce` is the nonce of the transaction, analogous to `Whiteboard.setMessage(...)`. 267 | - `gasPrice` is the gas price the contract is supposed to pay for the transaction, analogous to `Whiteboard.setMessage(...)`. 268 | - `to` is the recipient address. 269 | - `amount` is the amount in wei to be transferred to the address. 270 | - `signature` is the 65-byte signature of the transaction. 271 | It consists of the 32-byte `r`, 32-byte `s`, and 1-byte `v` values of the ECDSA signature over the hash of `contract._address, txNonce, gasPrice, to, amount`. 272 | 273 | #### Compile & Deploy Contract 274 | 275 | To compile the `Wallet` contract, do: 276 | 277 | ```shell 278 | solidity/build/solc/solc --bin --abi contracts/Wallet.sol 279 | ``` 280 | 281 | This should output both the contract bytecode, which we will reference as `__BYTECODE__`, as well as its ABI, which we will reference as `__ABI__`. 282 | 283 | To deploy the contract in `nodejs`, do: 284 | 285 | ```javascript 286 | const Web3 = require('web3'); 287 | let web3 = new Web3('http://localhost:8545'); 288 | let signer = '0x__SIGNER__'; 289 | let bytecode = '0x__BYTECODE__'; 290 | let abi = __ABI__; // note: no quotes! 291 | let Contract = new web3.eth.Contract(abi); 292 | let contract; 293 | Contract.deploy({data: bytecode}).send({from: signer, value: 10000000}).then(function(contractInstance){contract = contractInstance; console.log(contractInstance);}); 294 | ``` 295 | 296 | If you are using the same `nodejs` session as before, be sure to omit the `let` at the beginning of each statement for already declared variables, e.g. `bytecode = ...` instead of `let bytecode = ...` 297 | 298 | #### Send Transaction 299 | 300 | As an example transaction, you are going to send `42 wei` to the `0x0000000000000000000000000000000000000000` address. 301 | Before doing so, you can inspect the current chain state via: 302 | 303 | ```javascript 304 | let zeroAddress = '0x0000000000000000000000000000000000000000'; 305 | contract.options.from = '0xffffffffffffffffffffffffffffffffffffffff'; 306 | contract.methods.getNonce().call().then(console.log); 307 | contract.methods.getOwner().call().then(console.log); 308 | web3.eth.getBalance(contract._address).then(console.log); 309 | web3.eth.getBalance(zeroAddress).then(console.log); 310 | web3.eth.getBalance(signer).then(console.log); 311 | ``` 312 | 313 | As you can see, the signer account that deployed the AA contract is registered as its owner. 314 | To send a transfer, you thus first have to create and sign the transaction hash from the signer account: 315 | 316 | ```javascript 317 | let hash = web3.utils.soliditySha3(contract._address, 0, 1, zeroAddress, 42); 318 | let txSignature; 319 | web3.eth.personal.sign(hash, signer).then(function(signature){txSignature = signature;}); 320 | ``` 321 | 322 | You can then send the transfer as an AA transaction: 323 | 324 | ```javascript 325 | contract.methods.transfer(0, 1, zeroAddress, 42, txSignature).send({gasPrice: "0", gasLimit: 100000}).then(console.log); 326 | ``` 327 | 328 | After a few seconds, the transaction should be included in a block. 329 | The contract balance should be reduced by the sum of the transaction fee and the `42 wei` sent out. 330 | The balance of the zero address should now be `42 wei`. 331 | -------------------------------------------------------------------------------- /contracts/ECDSA.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Taken from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol 3 | 4 | pragma solidity ^0.6.0; 5 | 6 | /** 7 | * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. 8 | * 9 | * These functions can be used to verify that a message was signed by the holder 10 | * of the private keys of a given address. 11 | */ 12 | contract ECDSA { 13 | /** 14 | * @dev Returns the address that signed a hashed message (`hash`) with 15 | * `signature`. This address can then be used for verification purposes. 16 | * 17 | * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: 18 | * this function rejects them by requiring the `s` value to be in the lower 19 | * half order, and the `v` value to be either 27 or 28. 20 | * 21 | * IMPORTANT: `hash` _must_ be the result of a hash operation for the 22 | * verification to be secure: it is possible to craft signatures that 23 | * recover to arbitrary addresses for non-hashed data. A safe way to ensure 24 | * this is by receiving a hash of the original message (which may otherwise 25 | * be too long), and then calling {toEthSignedMessageHash} on it. 26 | */ 27 | function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { 28 | // Check the signature length 29 | if (signature.length != 65) { 30 | revert("ECDSA: invalid signature length"); 31 | } 32 | 33 | // Divide the signature in r, s and v variables 34 | bytes32 r; 35 | bytes32 s; 36 | uint8 v; 37 | 38 | // ecrecover takes the signature parameters, and the only way to get them 39 | // currently is to use assembly. 40 | // solhint-disable-next-line no-inline-assembly 41 | assembly { 42 | r := mload(add(signature, 0x20)) 43 | s := mload(add(signature, 0x40)) 44 | v := byte(0, mload(add(signature, 0x60))) 45 | } 46 | 47 | // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature 48 | // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines 49 | // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most 50 | // signatures from current libraries generate a unique signature with an s-value in the lower half order. 51 | // 52 | // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value 53 | // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or 54 | // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept 55 | // these malleable signatures as well. 56 | if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { 57 | revert("ECDSA: invalid signature 's' value"); 58 | } 59 | 60 | if (v != 27 && v != 28) { 61 | revert("ECDSA: invalid signature 'v' value"); 62 | } 63 | 64 | // If the signature is valid (and not malleable), return the signer address 65 | address signer = ecrecover(hash, v, r, s); 66 | require(signer != address(0), "ECDSA: invalid signature"); 67 | 68 | return signer; 69 | } 70 | 71 | /** 72 | * @dev Returns an Ethereum Signed Message, created from a `hash`. This 73 | * replicates the behavior of the 74 | * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] 75 | * JSON-RPC method. 76 | * 77 | * See {recover}. 78 | */ 79 | function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { 80 | // 32 is the length in bytes of hash, 81 | // enforced by the type signature above 82 | return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /contracts/Wallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.6.10; 4 | pragma experimental AccountAbstraction; 5 | 6 | import { ECDSA } from "./ECDSA.sol"; 7 | 8 | account contract Wallet is ECDSA { 9 | uint256 nonce; 10 | address owner; 11 | 12 | modifier paygasZero { 13 | assembly { paygas(0) } 14 | _; 15 | } 16 | 17 | function transfer(uint256 txNonce, uint256 gasPrice, address payable to, uint256 amount, bytes calldata signature) public { 18 | assert(nonce == txNonce); 19 | bytes32 hash = keccak256(abi.encodePacked(this, txNonce, gasPrice, to, amount)); 20 | bytes32 messageHash = toEthSignedMessageHash(hash); 21 | address signer = recover(messageHash, signature); 22 | require(signer == owner); 23 | nonce = txNonce + 1; 24 | assembly { paygas(gasPrice) } 25 | to.transfer(amount); 26 | } 27 | 28 | function getNonce() public view paygasZero returns (uint256) { 29 | return nonce; 30 | } 31 | 32 | function getOwner() public view paygasZero returns (address) { 33 | return owner; 34 | } 35 | 36 | constructor() public payable { 37 | owner = msg.sender; 38 | } 39 | } -------------------------------------------------------------------------------- /contracts/Whiteboard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.6.10; 4 | pragma experimental AccountAbstraction; 5 | 6 | account contract Whiteboard { 7 | uint256 nonce; 8 | string message; 9 | 10 | function setMessage(uint256 txNonce, uint256 gasPrice, string calldata newMessage, bool failAfterPaygas) public { 11 | require(nonce == txNonce); 12 | nonce = txNonce + 1; 13 | assembly { paygas(gasPrice) } 14 | message = newMessage; 15 | require(!failAfterPaygas); 16 | } 17 | 18 | function getNonce() public view returns (uint256) { 19 | assembly { paygas(0) } 20 | return nonce; 21 | } 22 | 23 | function getMessage() public view returns (string memory) { 24 | assembly { paygas(0) } 25 | return message; 26 | } 27 | 28 | constructor() public payable {} 29 | } -------------------------------------------------------------------------------- /data/genesis_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "chainId": 12345, 4 | "homesteadBlock": 0, 5 | "eip150Block": 0, 6 | "eip155Block": 0, 7 | "eip158Block": 0, 8 | "byzantiumBlock": 0, 9 | "constantinopleBlock": 0, 10 | "petersburgBlock": 0, 11 | "istanbulBlock": 0, 12 | "muirGlacierBlock": 0, 13 | "clique": { 14 | "period": 3, 15 | "epoch": 30000 16 | } 17 | }, 18 | "difficulty": "1", 19 | "gasLimit": "8000000", 20 | "extradata": "0x0000000000000000000000000000000000000000000000000000000000000000__SIGNER__0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 21 | "alloc": { 22 | "__SIGNER__": { "balance": "10000000000000000000000000" } 23 | } 24 | } --------------------------------------------------------------------------------