visit
‘Smart contracts’ is a misnomer. Despite its name, smart contracts on Ethereum are not self-executing digital agreements. Smart contract code only run when triggered by an . In other words, you need an external process to trigger the smart contract.
In this article, we’ll build a solution to this problem. You’ll learn:Finally, we’ll go through serverless-ethers, a fully-functional smart contract automation service that you can run and deploy out-of-the box. You can use this project as a base for building custom smart contract automation that fit your needs.
The sample application is open source and available on Github. Just clone and hit deploy!
Read on to learn why we need automation and how it works.
Imagine that we want to implement a smart contract with a function that should be automatically executed every 1 hour. ⏳⚙️
How can you accomplish this?
You can’t. This is not possible with plain Solidity smart contracts. Despite its name, ‘smart’ contracts in Ethereum are not self-executing. You need an external source (either human or machine) to call the smart contract and execute its code.
The most a contract can do is enforce a 1-hour interval between executions, for example:function runMe() public {
require(block.timestamp >= lastTriggeredAt + 1 hour);
...
}
However, somebody still needs to call the smart contract for the code to run in the first place.
Technically, it is possible to use to automatically execute certain operations. One example of this is ’s COMP distribution. Once an address has earned 0.001 COMP, any Compound transaction (e.g. supplying an asset, or transferring a cToken) will automatically transfer COMP to their wallet.
You can implement the above logic in a function modifier (a decorator), wrap the modifier around a function, and have the logic automatically executed whenever the function is called. The caller will pay the gas required for the additional logic.
However, not all smart contract systems follow this approach. One reason is that it can lead to unpredictable gas usage, since these modifiers may only run under certain conditions. It also forces additional gas fees onto a random subset of users, who just happened to be the unlucky few selected to ‘rebalance’ the contract.
Finally, somebody still needs to call the smart contract for the code to run.
> npm install -g serverless
> serverless -v
x.x.x
You can if you’re just interested in seeing things working. Read on to learn more about the Serverless framework.0. serverless.ymlAll of the Lambda functions and events in your Serverless service can be found in a configuration file called the . It defines a service with Functions and Events.
service: serverless-ethers
provider:
name: aws
runtime: nodejs12.x
environment:
CHAIN_ID: 3
DEFAULT_GAS_PRICE: 60000000000
functions:
myFunc:
handler: functions/myFunc.handler
events:
- schedule: rate(2 hours)
Under the
functions
property, you define your serverless functions. In the above example:myFunc
. handler
property points to the file and module containing the code you want to run in your function.events
property specifies Event triggers for the function to be executed.// functions/myFunc.js
exports.handler = async function(event, context) {
// Do anything
};
Events are the things that trigger your functions to run. Events belong to each Function and can be found in the events property in
serverless.yml
.You can use the trigger to automatically execute functions periodically. For example, to run the myFunc function every 2 hours we specify:# serverless.yml
functions:
myFunc:
handler: functions/myFunc.handler
events:
- schedule: rate(2 hours)
# serverless.yml
events:
- schedule: cron(0 12 * * ? *) # 12PM UTC
To learn more about the Serverless framework, check out .With the Serverless Framework basics out of the way, let’s jump into the service.
git clone [email protected]:yosriady/serverless-ethers.git
cd serverless-ethers
nvm use
npm install
The
serverless-ethers
project is structured as follows:├── contracts/
│ ├── abis/
│ ├── abis.js
│ └── addresses.js
├── functions/
│ └── exec.js
└── serverless.yml
contracts/
contain smart contract ABIs and addresses.functions/
contain JS functions that implement the business logic. serverless.yml
describe the service’s configuration.// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.6.10;
contract DummyStorage {
event Write(address indexed source, uint256 value);
uint internal _currentValue;
function get() public view returns (uint) {
return _currentValue;
}
function put(uint value) public {
emit Write(msg.sender, value);
_currentValue = value;
}
}
The
DummyStorage
smart contract has the following functions:get
is a read-only function that returns the contract’s current value.put
is a write function that updates the contract’s current value.The sample contract is verified and . Feel free to use it to test your functions!
├── contracts/
│ ├── abis/
│ │ └── DummyStorage.json
│ ├── abis.js
│ └── addresses.js
// functions/exec.js
const { abis, addresses } = require('../contracts');
const DummyStorageABI = abis.DummyStorage;
const DummyStorageAddress = addresses.DummyStorage;
// Initialize contract
const contract = new ethers.Contract(
DummyStorageAddress,
DummyStorageABI,
wallet,
)
// Call smart contract function `put(uint)`
const RANDOM_INTEGER = Math.floor(Math.random() * 100); // returns a random integer from 0 to 99
const tx = await contract.put(RANDOM_INTEGER)
Loading the contract ABI and address gives us an
ethers.Contract
abstraction with all the functions of our smart contract, including get()
and put()
.In the sample
exec
function, we call contract.put()
with a random integer.Before you can run the exec function, you’ll need to specify some environment variables in your serverless.yml:
# serverless.yml
service: serverless-ethers
provider:
name: aws
runtime: nodejs12.x
region: ap-southeast-1
timeout: 30
environment:
DEFAULT_GAS_PRICE: 60000000000
MNEMONIC: ...
SLACK_HOOK_URL: ...
serverless-ethers
uses the following environment variables:DEFAULT_GAS_PRICE
: Default gas price used when making write transactions.MNEMONIC
: 12-word mnemonic used to derive an Ethereum address. Make sure it’s if you intend to write data to Ethereum!SLACK_HOOK_URL
: The example sends messages to Slack using . You can get this URL from your Slack dashboard. (Optional)Important Note: make sure you do not store keys in plaintext in production. Use a secure parameter store such as when storing credentials such as mnemonics and API keys. Since every project has its own security requirements and setup, we leave it up to readers to decide how they want to approach storing secrets.
> serverless invoke local -f exec
Starting...
Contract ABIs loaded
Ethers wallet loaded
Contract loaded
Sending transaction...
:white_check_mark: Transaction sent //ropsten.etherscan.io/tx/0x72204f07911a319b4e5f7eb54ad15ed666cfc1403b53def40c9d60188b176383
Completed
true
Deploying is as easy as
serverless deplo
y:> serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service serverless-ethers.zip file to S3 (2.95 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.....................
Serverless: Stack update finished...
Service Information
service: serverless-ethers
stage: dev
region: ap-southeast-1
stack: serverless-ethers-dev
resources: 8
api keys:
None
endpoints:
None
functions:
exec: serverless-ethers-dev-exec
layers:
None
The sample application is open source and available on Github. Star the repo if you found it useful!
const successMessage = `:white_check_mark: Transaction sent //ropsten.etherscan.io/tx/${tx.hash}`;
await postToSlack(successMessage);
The
postToSlack
function makes use of a SLACK_HOOK_URL
environment variable that you can get from your . Once set up, you’ll be able to notify Slack whenever a transaction was sent successfully:It’s a nice and simple way to monitor your functions.// Given the following Event:
// event Transfer(bytes32 indexed node, address owner)
// Get the filter (the second null could be omitted)
const filter = contract.filters.Transfer(userAccount, null);
// Query the filter
const logs = contract.queryFilter(filter, 0, "latest"); // from block 0 to latest block
// Print out all the values:
logs.forEach((log) => {
console.log(log.args._to, log.args._value);
});
Previously published at