Building on Morph
tosynthegeek
Posted on May 18, 2024
Introduction
In the fast-evolving blockchain industry, scaling solutions have emerged as crucial for enhancing the scalability and efficiency of underlying blockchain infrastructures. Blockchains like Ethereum, once revolutionary, now struggle with the demands of a growing user base. Transaction fees have skyrocketed, and transaction throughput low, hindering its ability to handle the growing demand. Enter Morph, a next-generation blockchain platform designed to address these limitations.
The Morph Solution: Bridging Web3 gap by transitioning real-world applications Onchain
Morph is not just another scaling solution; it's a next-generation blockchain platform built from the ground up to address these critical limitations. Unlike traditional blockchains that store every transaction detail on-chain, Morph leverages a unique architecture that combines established and cutting-edge technologies. This unique blend empowers developers to build powerful and user-friendly applications without sacrificing scalability, security, or efficiency.
Morph's Architecture: Core Functionalities
Morph is designed with a modular architecture that consists of three functional modules that effectively collaborate with one another while preserving their individual autonomy:
- Decentralized Sequencer Network for Consensus and Execution, ensuring fair and transparent transaction processing through a decentralized network of nodes, reducing the risk of censorship and centralization.
- Rollup for Data Availability, ensuring that transaction data is stored and accessible off-chain while keeping the on-chain data footprint minimal. Rollups aggregate multiple transactions into a single batch, which is then recorded on the blockchain. This process significantly reduces the amount of data that needs to be stored on-chain, thereby improving scalability and reducing costs.
- Optimistic zkEVM for Settlement of transactions, validating them off-chain and only submitting proofs to the blockchain when necessary.
Read more on Morph Modular Design
Building Applications on Morph
Good news: It's just like building on Ethereum! There are very few changes needed. So if you're an Ethereum builder, you are already a Morph builder. 😎
In the next part, we'll be deploying a smart contract to the Morph Holesky Testnet. It's the same code from my Confidential Smart Contracts post.
Getting Started
- Metamask Wallet Setup for Morph Testnet: Crypto wallet.
- Hardhat: Ethereum development environment. It comes as an npm package.
- Morph Holesky Testnet RPC URL
- Get some Morph Testnet Ethereum here
Set up environment
Now that we've gathered our tools, it's time to set up our development environment. Here's a step-by-step guide:
- Start by running the following commands:
mkdir auction
cd auction
npm init -y
npm install --save-dev hardhat (We will be using typescript)
npx hardhat init
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm i dotenv
code .
- To keep sensitive information like your Metamask private key, create a .env file in your project directory and store your keys there in the format below:
PRIVATE_KEY=""
- Modify your Hardhat configuration file (hardhat.config.ts) to recognize the keys from your .env file. Also, add morph testnet as a network.
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.24",
networks: {
morph: {
url: "https://rpc-quicknode-holesky.morphl2.io",
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
},
};
export default config;
Building and Compiling the Contract
In the /contracts
directory of your Hardhat project, create a new file named Auction.sol. I've added the smart contract below with some comments to explain what's going on.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Auction {
// Auction details
address payable public seller; // Address of the seller
uint256 public startingPrice; // Starting price of the auction
uint256 public highestBid; // Highest bid amount
address public highestBidder; // Address of the highest bidder
uint256 public auctionEndTime; // Timestamp when the auction ends
bool public auctionEnded; // Flag to indicate if the auction has ended
uint256 public bidCount;
// Struct to store bid details
struct Bid {
address bidder;
uint256 bid;
}
Bid[] public bids; // Array to store all bids
// Events for auction lifecycle
event BidCreated(address bidder, uint256 amount);
event AuctionEnded(address winner, uint256 winningBid);
// Constructor to initialize auction parameters
constructor(
address payable _seller, // Address of the seller
uint256 _startingPrice, // Starting price of the auction
uint256 _auctionEndTime // Duration of the auction
) {
seller = _seller;
startingPrice = _startingPrice;
highestBid = startingPrice;
auctionEndTime = block.timestamp + _auctionEndTime; // Set the end time of the auction
}
/**
* @notice Allows a user to place a bid in the auction.
* @dev The bid must be higher than the current highest bid, and the auction must not have ended.
*/
function bid() external payable {
require(block.timestamp < auctionEndTime, "Auction has ended");
require(
msg.value > highestBid,
"Bid must be higher than the current highest bid"
);
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBid = msg.value;
highestBidder = msg.sender;
Bid memory newBid = Bid(msg.sender, msg.value);
bids.push(newBid);
bidCount++;
emit BidCreated(msg.sender, msg.value);
}
/**
* @notice Allows the highest bidder or seller to check the bid at a given index.
* @dev The auction must have ended, and only the highest bidder or seller can check bids.
* @param index The index of the bid to check.
* @return The bidder's address and bid amount.
*/
function checkBid(uint256 index) external view returns (address, uint256) {
require(index <= bidCount, "Wrong Index");
Bid memory getBid = bids[index];
require(block.timestamp > auctionEndTime, "Auction is still ongoing");
require(
msg.sender == highestBidder || msg.sender == seller,
"Only Highest Bidder and seller can check bids"
);
return (getBid.bidder, getBid.bid);
}
/**
* @notice Returns the timestamp when the auction ends.
* @return The timestamp when the auction ends.
*/
function checkAuctionEndTime() external view returns (uint256) {
return auctionEndTime;
}
/**
* @notice Ends the auction and transfers the highest bid amount to the seller.
* @dev The auction must have ended.
*/
function endAuction() external {
require(block.timestamp >= auctionEndTime, "Auction is still ongoing");
auctionEnded = true; // Update auction status
seller.transfer(highestBid); // Transfer the highest bid to the seller
emit AuctionEnded(highestBidder, highestBid); // Emit AuctionEnded event
}
/**
* @notice Returns the auction details.
* @return The seller's address, starting price, highest bid amount, highest bidder's address,
* auction end timestamp, auction end status, and total bid count.
*/
function getAuctionDetails()
external
view
returns (address, uint256, uint256, address, uint256, bool, uint256)
{
return (
seller,
startingPrice,
highestBid,
highestBidder,
auctionEndTime,
auctionEnded,
bidCount
);
}
}
Compile the contract using the command: npx hardhat compile
npx hardhat compile
Compiled 1 Solidity file successfully
Deploying and Interacting with the Contract
Now that you have your contract compiled, the next step would be to write a script to deploy and interact with your contract. In the /scripts
folder, you'll find a default script, deploy.ts
, you can delete that. Go ahead and create a new file called run-auction.ts
and add the following code into it:
import hre from "hardhat";
async function getStorageAt(address: string, slotNumber: string) {
const provider = hre.ethers.provider;
const result = await provider.getStorage(address, slotNumber);
return result.toString();
}
/**
* @notice Decode the transaction input data using the ABI.
* @param abi The ABI of the contract.
* @param inputData The input data of the transaction.
* @return The decoded transaction data, or an empty object if decoding fails.
*/
function decodeTransactionInput(abi: any[], inputData: string) {
try {
const iauction = new hre.ethers.Interface(abi);
const decodedData = iauction.parseTransaction({ data: inputData});
return decodedData;
} catch (error) {
console.error("Error decoding transaction input:");
return { args: [] };
}
}
async function main(value: number) {
const index = 0;
const address = "0xDA01D79Ca36b493C7906F3C032D2365Fb3470aEC";
const Auction = await hre.ethers.getContractFactory("Auction");
const auction = await Auction.deploy(
"0xd109e8c395741b4b3130e3d84041f8f62af765ef",
100,
60 // 10 minutes for the auction duration
);
console.log("Auction contract deployed on Morph to: ",
await auction.getAddress()
);
console.log("Bidding....");
const tx = await auction.bid({
value: value.toString(),
});
await tx.wait();
console.log("Bid successful!");
// Trying to get the bid of an associated address
try {
console.log("Checking bid at Index: ", index);
const bid = await auction.checkBid(index);
console.log("Bid at Index 0 is:", bid);
} catch (error) {
console.error("Failed to check bid: Auction is still ongoing");
}
console.log("Waiting....");
const endTime = await auction.checkAuctionEndTime();
console.log("Auction endtime is: ", endTime);
console.log("Still waiting....");
try {
await new Promise((resolve) => setTimeout(resolve, 100_000));
console.log("Checking bid again");
const bid = await auction.checkBid(index);
console.log("Bid:", bid);
} catch (error) {
console.log("Failed to check bid: Auction is still ongoing");
}
const decodedInput = decodeTransactionInput(
auction.interface.format(),
tx.data
);
console.log("Decoded data input: ", decodedInput?.args);
const StateData = await getStorageAt(await auction.getAddress(), "0x0");
console.log("State data at slot 0x0 is: ", StateData);
}
main(120).catch((error) => {
console.error(error);
process.exitCode = 1;
});
This script demonstrates how to interact with the auction contract by performing several actions:
- Deployment: Deploys the contract, initializing it with the starting price and auction duration.
- Bidding: Places a bid in the auction, confirming the transaction hash.
- Bid Access Control: Attempts to check a specific bid while the auction is ongoing, showcasing the access restriction that only allows the highest bidder or seller to view bid details during the auction. It then successfully checks the bid details after the auction ends.
- Transaction Input Decoding: Decodes the transaction input data used for checking the bid, extracting relevant information like the bid amount.
- State Retrieval Simulation: Simulates retrieving state data at a specific slot.
Deploying to Morph Testnet
To deploy to Morph testnet, run the command:
npx hardhat run scripts/run-auction.ts --network morph
You should get an output that looks like this:
Auction contract deployed on Morph to: 0x290946a5f508530023e08260B67957f408D6dB75
Bidding....
Bid successful!
Checking bid at Index: 0
Failed to check bid: Auction is still ongoing Waiting....
Auction endtime is: 1715895486n Still waiting....
Checking bid again
Bid: Result(2) [ '0xDA01D79Ca36b493C7906F3C032D2365Fb3470aEC', 120n ]
Decoded data input: Result(0) []
State data at slot 0x0 is: 0x000000000000000000000000d109e8c395741b4b3130e3d84041f8f62af765ef
This means your contract has been successfully deployed to the Morph Test Network!
Head over to the Morph Testnet Explorer to scan your contract address.
What's Next?
To continue building on the Morph network, I recommend the following resources:
- Morph Developer Docs: for technical specifications and functionalities.
- Discord Channel: Join the Morph Community to get feedback and share thoughts.
- Morph Blog: Also check out the Morph Blog for News, Announcements and Guides like this.
Posted on May 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.