Creating a Smart Contract

We will start by developing a very simple smart contract, a “timelock” that accepts a payment of ETH when the contract is created, and only allows that ETH to be withdrawn by the creator of the contract, and after a specified delay.

Create a development repository

In prior tutorials, we’ve run sbt by executing the ./sbtw wrapper script within the eth-command-line repository.

Now we will need to make our own repository, for our own newly developed smart contracts.

It’s best to install the sbt command on your command line before developing your own smart contracts. Once you’ve done that, we can get started.

The easiest way to create a fresh sbt-ethereum repository is to use the giter8 template swaldman/solidity-seed.g8:

$ sbt new swaldman/solidity-seed.g8

You’ll be prompted with some interactive queries:

[info] Loading project definition from /Users/testuser/project
[info] Set current project to testuser (in build file:/Users/testuser/)

A minimal solidity project for sbt-ethereum 

name [my-solidity-project]: eth-timelock
version [0.0.1-SNAPSHOT]: 
sbt_ethereum_version [0.1.7-SNAPSHOT]: 
sbt_version [1.2.8]: 

Template applied in ./eth-timelock

Voila! You’ll find a new directory called eth-timelock has been created. It’s a fully functional sbt-ethereum repository. If you cd into that directory and type sbt, you will get an sbt-ethereum command line just as rich as the eth-command-line we’ve used previously.

Note

Use version control software!

If you use git or similar version control software, now would be a good time to initialize the repository and perform an initial commit!

 

Define your smart contract source code

In your repository, you will find a folder called src/main/solidity. Solidity is the most common language for developing Ethereum smart contracts. Any files that you place in this directory that end in the suffix .sol will be compiled by sbt-ethereum as Solidity smart contracts.

Create a file in this directory called Timelock.sol, and paste in the following contents:

pragma solidity >=0.4.24;

contract Timelock {
  address public owner;
  uint public releaseDate;

  constructor( uint _days, uint _seconds ) public payable {
    require( msg.value > 0, "There's no point in creating an empty Timelock!" );
    owner = msg.sender;
    releaseDate = now + (_days * 1 days) + (_seconds * 1 seconds);
  }

  function withdraw() public {
    require( msg.sender == owner, "Only the owner can withdraw!" );
    require( now > releaseDate, "Cannot withdraw prior to release date!" );
    msg.sender.transfer( address(this).balance );
  }
}

Compile the source code

From inside your eth-timelock directory, type sbt, then the command compile:

$ sbt
[info] Loading settings for project eth-timelock from build.sbt ...
[info] Set current project to eth-timelock (in build file:/Users/testuser/eth-timelock/)
[info] Updating available solidity compiler set.
[info] sbt-ethereum-0.1.7-SNAPSHOT successfully initialized (built Sun, 17 Feb 2019 21:58:11 -0800)
sbt:eth-timelock> compile
[info] Compiling 1 Solidity source to /Users/testuser/eth-timelock/target/ethereum/solidity...
[info] Compiling 'Timelock.sol'. (Debug source: '/Users/testuser/eth-timelock/target/ethereum/solidity/Timelock.sol')
[info] No Scala stubs will be generated as the setting 'ethcfgScalaStubsPackage' has not ben set.
[info] If you'd like Scala stubs to be generated, please define 'ethcfgScalaStubsPackage'.
[info] Updating ...
[info] Done updating.
[success] Total time: 1 s, completed Feb 25, 2019 12:16:01 AM

Deploying the contract

Once your smart contract has been compiled, you should be able to deploy it. The command you will want to use is ethTransactionDeploy

The smart contract we are deploying has a constructor that includes arguments, and is payable. Let’s use tab completion to check that out:

sbt:eth-timelock> ethTransactionDeploy <tab>
Timelock

If we <tab> after the ethTransactionDeploy command, we get a list of contracts that we can deploy, that have been compiled within our repository. In our case, the only contract is Timelock. Let’s tab again to see what’s next:

sbt:eth-timelock> ethTransactionDeploy Timelock 
<_days, of type uint256>   ​

The next constructor argument is the number of days delay the contract will enforce before permiting us to withdraw whatever we have paid into it. We don’t want to wait days to test this thing, so let’s just say 0, and hit <tab> again.

sbt:eth-timelock> ethTransactionDeploy Timelock 0 <tab>
<_seconds, of type uint256>

In addition to some number of days, the contract allows us to specify a number of seconds in the delay it will enforce. Let’s try a 10 minute — that is, 600 second, delay.

sbt:eth-timelock> ethTransactionDeploy Timelock 0 600 <tab>
[ETH to pay, optional]   ether                    finney                   gwei                     szabo                    wei

Now the contract is asking for an optional ETH amount to pay. As we saw in a previous tutorial, we specify ETH amounts as a number and a unit, one of wei, gwei, szabo, finney, or ether. A wei is the tiniest unit (it represents 10-18 ETH). Since we are only testing this contract out, let’s deposit only 1 wei.

sbt:eth-timelock> ethTransactionDeploy Timelock 0 600 1 wei
[info] Unlocking address '0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2' (on chain with ID 1, aliases ['default-sender'])
Enter passphrase or hex private key for address '0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2': ***************
[info] V3 wallet(s) found for '0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2' (aliases ['default-sender'])
[info] Waiting for the transaction to be mined (will wait up to 5 minutes).

==> T R A N S A C T I O N   S U B M I S S I O N   R E Q U E S T
==>
==> The transaction would be a contract creation with...
==>   From:  0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2 (with aliases ['default-sender'] on chain with ID 1)
==>   Init:  0x6080604081815280610350833981016040528051602090910151600034116100ae57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602f60248201527f54686572652773206e6f20706f696e7420696e206372656174696e6720616e2060448201527f656d7074792054696d656c6f636b210000000000000000000000000000000000606482015290519081900360840190fd5b60008054600160a060020a0319163317905542620151809092029190910101600155610271806100df6000396000f3006080604052600436106100565763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416633ccfd60b811461005b5780638da5cb5b14610072578063b9e3e2db146100b0575b600080fd5b34801561006757600080fd5b506100706100d7565b005b34801561007e57600080fd5b50610087610223565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b3480156100bc57600080fd5b506100c561023f565b60408051918252519081900360200190f35b60005473ffffffffffffffffffffffffffffffffffffffff16331461015d57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f4f6e6c7920746865206f776e65722063616e2077697468647261772100000000604482015290519081900360640190fd5b60015442116101f357604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f43616e6e6f74207769746864726177207072696f7220746f2072656c6561736560448201527f2064617465210000000000000000000000000000000000000000000000000000606482015290519081900360840190fd5b6040513390303180156108fc02916000818181858888f19350505050158015610220573d6000803e3d6000fd5b50565b60005473ffffffffffffffffffffffffffffffffffffffff1681565b600154815600a165627a7a72305820cce6ece2186b4f5d31d2fefcac36b5c0d30fff3f4f0dabce03115f12361b6e09002900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000258
==>   Value: 1E-18 Ether
==>
==> The nonce of the transaction would be 7.
==>
==> $$$ The transaction you have requested could use up to 317328 units of gas.
==> $$$ You would pay 18 gwei for each unit of gas, for a maximum cost of 0.005711904 ether.
==> $$$ This is worth 0.785758073760 USD (according to Coinbase at 1:05 AM).
==> You would also send 1E-18 ether (1.37565E-16 USD), for a maximum total cost of 0.005711904000000001 ether (0.785758073760000137565 USD).

Would you like to submit this transaction? [y/n] y

We wait a while, then…

A transaction with hash '0xf58f30d3ea1d077d8054d77b4c03aca333e49e905b47182c2a7d74d4f026e861' will be submitted. Please wait.
[info] Transaction Receipt:
[info]        Transaction Hash:    0xf58f30d3ea1d077d8054d77b4c03aca333e49e905b47182c2a7d74d4f026e861
[info]        Transaction Index:   107
[info]        Transaction Status:  SUCCEEDED
[info]        Block Hash:          0xdf5237645e7cfb9dc95c2a89756bb55528c3adefadcc8d858223671642982a0c
[info]        Block Number:        7265310
[info]        From:                0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2
[info]        To:                  Unknown
[info]        Cumulative Gas Used: 7553899
[info]        Gas Used:            264440
[info]        Contract Address:    0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03
[info]        Logs:                None
[info]        Events:              None
[info] Contract 'Timelock' deployed in transaction '0xf58f30d3ea1d077d8054d77b4c03aca333e49e905b47182c2a7d74d4f026e861'.
[info] Contract 'Timelock' has been assigned address '0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03'.
Enter an optional alias for the newly deployed 'Timelock' contract at '0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03' (or [return] for none):

It asks us for an alias for our new contract’s address. Let’s give it one:

Enter an optional alias for the newly deployed 'Timelock' contract at '0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03' (or [return] for none): timelock
[info] Alias 'timelock' now points to address '0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03' (for chain with ID 1).
[info] Refreshing caches.
[success] Total time: 47 s, completed Feb 25, 2019 1:05:40 AM

Our contract is deployed! We can take a look in Etherscan by searching for its new address 0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03.

etherscan-new-timelock

Trying out the contract

Quickly, quickly, within the 10 minute delay, let’s try to see what happens if we attempt an “early withdrawal”. Let’s try the command ethTransactionMock, which asks our Ethereum node to simulate, but does not actually, permanently, execute a transaction. We don’t expect this transaction to work anyway, and we don’t want to pay for or actually make a change to the blockchain while trying.

sbt:eth-timelock> ethTransactionMock timelock withdraw
[warn] Simulating the result of calling nonconstant function 'withdraw'.
[warn] An actual transaction would occur sometime in the future, with potentially different results!
[warn] No changes that would have been made to the blockchain by a call of this function will be preserved.
[error] com.mchange.sc.v2.jsonrpc.package$JsonrpcException: gas required exceeds allowance or always failing transaction [code=-32000]: No further information
[error] 	at com.mchange.sc.v2.jsonrpc.Response$Error.vomit(Response.scala:12)
[error] 	at com.mchange.sc.v1.consuela.ethereum.jsonrpc.Client$Implementation$Exchanger.$anonfun$responseHandler$1(Client.scala:282)
[error] 	at scala.util.Success.$anonfun$map$1(Try.scala:251)
[error] 	at scala.util.Success.map(Try.scala:209)
[error] 	at scala.concurrent.Future.$anonfun$map$1(Future.scala:288)
[error] 	at scala.concurrent.impl.Promise.liftedTree1$1(Promise.scala:29)
[error] 	at scala.concurrent.impl.Promise.$anonfun$transform$1(Promise.scala:29)
[error] 	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
[error] 	at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
[error] 	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
[error] 	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
[error] 	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
[error] 	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
[error] (Compile / ethTransactionMock) com.mchange.sc.v2.jsonrpc.package$JsonrpcException: gas required exceeds allowance or always failing transaction [code=-32000]: No further information
[error] Total time: 0 s, completed Feb 25, 2019 1:10:05 AM

The error “gas required exceeds allowance or always failing transaction [code=-32000]: No further information” is what you often see if a requirement fails or something causes the transaction to revert. This is an "always failing transaction’, in the sense that for the moment, no matter how much gas you give it, it would fail, since withdrawal is not permitted until the delay has passed.

Now let’s wait until our 10 minute delay has passed, and simulate our transaction again.

sbt:eth-timelock> ethTransactionMock timelock withdraw
[warn] Simulating the result of calling nonconstant function 'withdraw'.
[warn] An actual transaction would occur sometime in the future, with potentially different results!
[warn] No changes that would have been made to the blockchain by a call of this function will be preserved.
[info] The function withdraw yields no result.
[success] Total time: 0 s, completed Feb 25, 2019 1:25:43 AM

It looks like our transaction would have worked, but if we check Etherscan we will see that nothing has happened. Our contract still has a balance of the 1 wei we sent it, and there are no transactions to it beyond the contract creation. That’s becuase we’ve used ethTransactionMock, which yields only a simulation. To actually withdraw the 1 wei our contract holds, we have to (after the delay period has ended) use ethTransactionInvoke:

sbt:eth-timelock> ethTransactionInvoke timelock withdraw
[info] Unlocking address '0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2' (on chain with ID 1, aliases ['default-sender'])
Enter passphrase or hex private key for address '0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2': ***************
[info] V3 wallet(s) found for '0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2' (aliases ['default-sender'])

==> T R A N S A C T I O N   S U B M I S S I O N   R E Q U E S T
==>
==> The transaction would be a message with...
==>   To:    0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03 (with aliases ['timelock'] on chain with ID 1)
==>   From:  0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2 (with aliases ['default-sender'] on chain with ID 1)
==>   Data:  0x3ccfd60b
==>   Value: 0 Ether
==>
==> According to the ABI currently associated with the 'to' address, this message would amount to the following method call...
==>   Function called: withdraw()
==>
==> The nonce of the transaction would be 8.
==>
==> $$$ The transaction you have requested could use up to 38392 units of gas.
==> $$$ You would pay 12 gwei for each unit of gas, for a maximum cost of 0.000460704 ether.
==> $$$ This is worth 0.063413602080 USD (according to Coinbase at 1:28 AM).

Would you like to submit this transaction? [y/n] y
A transaction with hash '0x9f36bc1dc473198fb952f388302e33823d3dbac2514c75c849b2fedc7960e30f' will be submitted. Please wait.

Now we wait a few minutes, until…

[info] Called function 'withdraw', with args '', sending 0 wei to address '0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03' in transaction '0x9f36bc1dc473198fb952f388302e33823d3dbac2514c75c849b2fedc7960e30f'.
[info] Waiting for the transaction to be mined (will wait up to 5 minutes).
[info] Transaction Receipt:
[info]        Transaction Hash:    0x9f36bc1dc473198fb952f388302e33823d3dbac2514c75c849b2fedc7960e30f
[info]        Transaction Index:   99
[info]        Transaction Status:  SUCCEEDED
[info]        Block Hash:          0x3df21be080343808e95da771aae138a9c8477f3d48d1cd62e00e1288235b9dbf
[info]        Block Number:        7265385
[info]        From:                0x1144f4f7aad0c463c667e0f8d73fc13f1e7e86a2
[info]        To:                  0x3e24bfe40874a2f366ecf05746d6dcbc0cfd6f03
[info]        Cumulative Gas Used: 4816236
[info]        Gas Used:            29739
[info]        Contract Address:    None
[info]        Logs:                None
[info]        Events:              None
[success] Total time: 102 s, completed Feb 25, 2019 1:30:31 AM

Now, if we refresh Etherscan, we’ll see that the 1 wei we paid into the contract on construction is gone. The contract balance is 0 ether. To see the transaction in which the 1 wei was sent back to the contract’s owner, check the Internal Txns tab.