Quickstart: Build a decentralized app (Solidity)
Head over to the Stylus quickstart if you'd like to use Rust instead of Solidity.
This quickstart is for web developers who want to start building decentralized applications (dApps) using Arbitrum. It makes no assumptions about your prior experience with Ethereum, Arbitrum, or Solidity. Familiarity with Javascript and yarn is expected. If you're new to Ethereum, consider studying the Ethereum documentation before proceeding.
What we'll learn
In this tutorial we will learn:
- The basics of Ethereum vs. client/server architecture
- What is a Solidity smart contract
- How to compile and deploy a smart contract
- How to use an Ethereum wallet
We're going to build a digital cupcake vending machine using Solidity smart contracts1. This vending machine will follow two rules:
- The vending machine will distribute a cupcake to anyone who hasn't recently received one.
- The vending machine's rules can't be changed by anyone.
Here's the vending machine implemented with Javascript. To use it, enter a name in the form below and press the 'Cupcake please!' button, you should see your cupcake balance go up.
We can assume that this vending machine operates as we expect, but it's largely up to the centralized service provider that hosts it. In the case of a compromised cloud host:
- Our centralized service provider can deny access to particular users.
- A malicious actor can change the rules of the vending machine at any time, for example, to give their friends extra cupcakes.
Centralized third-party intermediaries represent a single point of failure that malicious actors can exploit. With a blockchain infrastructure such as Ethereum, we decentralize our vending machine's business logic and data, making this type of exploits nearly impossible.
This is Arbitrum's core value proposition to you, dear developer. Arbitrum makes it easy for you to deploy your vending machines to Ethereum's permissionless, trustless, decentralized network of nodes2 while keeping costs low for you and your users.
Let's implement the "Web3" version of the above vending machine using Arbitrum.
Prerequisites
VS Code
VS Code is the IDE we'll use to build our vending machine. See code.visualstudio.com to install.
Web3 wallet
We will use Metamask as the wallet to interact with our vending machine. See metamask.io and click View MetaMask Web or OKX Wallet and click Connect Wallet to install.
Yarn
Yarn is the package manager we'll use to install dependencies. See yarnpkg.com to install.
Foundry
Foundry is the toolchain we'll use to compile and deploy our smart contract. See getfoundry.sh to install.
We'll address any remaining dependencies as we go.
Ethereum and Arbitrum in a nutshell
- Ethereum
- Ethereum is a decentralized network of nodes that use Ethereum's client software (like Offchain's Prysm to maintain a public blockchain data structure.
- The data within Ethereum's blockchain data structure changes one transaction at a time.
- Smart contracts are small programs that execute transactions according to predefined rules. Ethereum's nodes host and execute smart contracts.
- You can use smart contracts to build decentralized apps (dApps) that use Ethereum's network to process transactions and store data. Think of smart contracts as your dApp's backend
- DApps let users carry their data and identity between applications without trusting centralized service providers.
- People who run Ethereum validator nodes3 can earn
$ETHfor processing and validating transactions on behalf of users and dApps. - These transactions can be expensive when the network is under heavy load.
- Arbitrum
- Arbitrum is a suite of child chain scaling solutions for dApp developers.
- Arbitrum One is a child chain that implements the Arbitrum Rollup protocol.
- You can use Arbitrum One to build user-friendly dApps with high throughput, low latency, and low transaction costs while inheriting Ethereum's high-security standards4.
Review the Javascript vending machine
Here's the vending machine implemented as a Javascript class:
VendingMachine.js
class VendingMachine {
// state variables = internal memory of the vending machine
cupcakeBalances = {};
cupcakeDistributionTimes = {};
// Vend a cupcake to the caller
giveCupcakeTo(userId) {
if (this.cupcakeDistributionTimes[userId] === undefined) {
this.cupcakeBalances[userId] = 0;
this.cupcakeDistributionTimes[userId] = 0;
}
// Rule 1: The vending machine will distribute a cupcake to anyone who hasn't recently received one.
const fiveSeconds = 5000;
const userCanReceiveCupcake = this.cupcakeDistributionTimes[userId] + fiveSeconds <= Date.now();
if (userCanReceiveCupcake) {
this.cupcakeBalances[userId]++;
this.cupcakeDistributionTimes[userId] = Date.now();
console.log(`Enjoy your cupcake, ${userId}!`);
return true;
} else {
console.error(
'HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)',
);
return false;
}
}
getCupcakeBalanceFor(userId) {
return this.cupcakeBalances[userId];
}
}
The VendingMachine class uses state variables and functions to implement predefined rules. This implementation is useful because it automates cupcake distribution, but there's a problem: it's hosted by a centralized server controlled by a third-party service provider.
Let's decentralize our vending machine's business logic and data by porting the above JavaScript implementation into a Solidity smart contract.
Review the Solidity vending machine
Here is a Solidity implementation of the vending machine. Solidity is a language that compiles to EVM bytecode. This means that it is deployable to any Ethereum-compatible blockchain, including Ethereum mainnet, Arbitrum One, and Arbitrum Nova.
VendingMachine.sol
// SPDX-License-Identifier: MIT
// Specify the Solidity compiler version - this contract requires version 0.8.9 or higher
pragma solidity ^0.8.9;
// Define a smart contract named VendingMachine
// Unlike regular classes, once deployed, this contract's code cannot be modified
// This ensures that the vending machine's rules remain constant and trustworthy
contract VendingMachine {
// State variables are permanently stored in blockchain storage
// These mappings associate Ethereum addresses with unsigned integers
// The 'private' keyword means these variables can only be accessed from within this contract
mapping(address => uint) private _cupcakeBalances; // Tracks how many cupcakes each address owns
mapping(address => uint) private _cupcakeDistributionTimes; // Tracks when each address last received a cupcake
// Function to give a cupcake to a specified address
// 'public' means this function can be called by anyone
// 'returns (bool)' specifies that the function returns a boolean value
function giveCupcakeTo(address userAddress) public returns (bool) {
// Initialize first-time users
// In Solidity, uninitialized values default to 0, so this check isn't strictly necessary
// but is included to mirror the JavaScript implementation
if (_cupcakeDistributionTimes[userAddress] == 0) {
_cupcakeBalances[userAddress] = 0;
_cupcakeDistributionTimes[userAddress] = 0;
}
// Calculate when the user is eligible for their next cupcake
// 'seconds' is a built-in time unit in Solidity
// 'block.timestamp' gives us the current time in seconds since Unix epoch
uint fiveSecondsFromLastDistribution = _cupcakeDistributionTimes[userAddress] + 5 seconds;
bool userCanReceiveCupcake = fiveSecondsFromLastDistribution <= block.timestamp;
if (userCanReceiveCupcake) {
// If enough time has passed, give them a cupcake and update their last distribution time
_cupcakeBalances[userAddress]++;
_cupcakeDistributionTimes[userAddress] = block.timestamp;
return true;
} else {
// If not enough time has passed, revert the transaction with an error message
// 'revert' cancels the transaction and returns the error message to the user
revert("HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)");
}
}
// Function to check how many cupcakes an address owns
// 'public' means anyone can call this function
// 'view' means this function only reads data and doesn't modify state
// This makes it free to call (no gas cost) when called externally
function getCupcakeBalanceFor(address userAddress) public view returns (uint) {
return _cupcakeBalances[userAddress];
}
}
Compile your smart contract with Remix
Smart contracts need to be compiled to bytecode to be stored and executed on-chain by the EVM; we'll use Remix to do that.
Remix is a browser-based IDE for EVM development. There are other IDEs to choose from (Foundry, Hardhat), but Remix doesn't require any local environment setup, so we'll choose it for this tutorial.
Let's first add our smart contract to Remix following these steps: