Getting started building contracts
The LUKSO ecosystem offers smart contract developers a lot of new standards and tools to build powerful, modular, and standardized blockchain applications. As LUKSO is an EVM-based Blockchain, all tools and tutorials for Ethereum also work well for LUKSO.
The following tutorial will teach you how to:
- set up a Hardhat project using TypeScript
- configure the contract settings, networks and Blockscout API
- utilize LSP smart contract presets using the
@lukso/lsp-smart-contracts
- compile and deploy LSP-based smart contracts
- verify smart contracts on Blockscout
If you need more low level information about our contracts, you can check the dedicated contracts section.
Playground Repositoryâ
Want to dive into the code directly? The lukso-playground
repository has a full Hardhat setup, including ready-to-go network configurations, sample contracts, and scripts to deploy and verify LSP-based contracts on LUKSO networks using both EOAs or Universal Profiles.
Set up a Hardhat projectâ
First, create a new folder for the Hardhat repository:
mkdir lukso-hardhat-repo
cd lukso-hardhat-repo
Afterward, initialize the Hardhat project using TypeScript:
npx hardhat init
# Proceed installing the Hardhat library
# select 'Create a TypeScript project'
# use the default values for the rest of the setup
# This will install the toolbox for deployment and verification
# This will also add a gitignore file
Once finished, you have a working Hardhat setup. To use your private keys and Universal Profile address for deployment, please set up the dotenv
environment, so those can be stored and fetched in a private file.
npm install -D dotenv
After installation, we will update the .gitignore
file, so library installations and private keys from dotenv
are never included in any code commits within your project. Especially dotenv
files contain sensitive values such as the private key of a blockchain account.
node_modules
.env
You can then go ahead to create a .env
file within the root folder of the Hardhat repository and the following contents:
PRIVATE_KEY=0x...
UP_ADDR=0x...
The private key will be used to deploy the smart contracts to the network and can either be a standalone Externally Owned Account or a controller of a Universal Profile. If you want to deploy the smart contracts as a Universal Profile, you also have to provide the related address of the Universal Profile.
Never share your private key that you will put into the .env
file!
To pay for the deployment fees of the accounts, you will need LYXt or LYX. For the testnet, you can request LYXt from the LUKSO Testnet faucet
We now have a base Hardhat setup that we can use to develop and deploy our smart contracts.
Configure contracts and networksâ
By default, the deployment will be to your local network and default Solidity version. If you want to deploy and verify your smart contracts on LUKSO, you will have to
- specify
solidity
compiler settings, so LSP presets match your contract version and EVM requirements - add the parameters of the
networks
and their related private keys - define the
etherscan
APIs to verify contracts on the networks
import { HardhatUserConfig } from 'hardhat/config';
import { config as LoadEnv } from 'dotenv';
import '@nomicfoundation/hardhat-toolbox';
LoadEnv();
const config: HardhatUserConfig = {
solidity: {
// Default compiler version for all contracts
version: '0.8.19',
settings: {
optimizer: {
enabled: true,
runs: 1000,
},
},
},
networks: {
luksoTestnet: {
url: 'https://4201.rpc.thirdweb.com',
chainId: 4201,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
luksoMainnet: {
url: 'https://42.rpc.thirdweb.com',
chainId: 42,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
etherscan: {
apiKey: 'no-api-key-needed',
customChains: [
{
network: 'luksoTestnet',
chainId: 4201,
urls: {
apiURL: 'https://explorer.execution.testnet.lukso.network/api',
browserURL: 'https://explorer.execution.testnet.lukso.network/',
},
},
{
network: 'luksoMainnet',
chainId: 42,
urls: {
apiURL: 'https://explorer.execution.mainnet.lukso.network/api',
browserURL: 'https://explorer.execution.mainnet.lukso.network/',
},
},
],
},
};
export default config;
Use LSP smart contract presetsâ
To use LSP smart contracts within your Hardhat project, you can install the latest version of the @lukso/lsp-smart-contracts
package like the following:
npm install @lukso/lsp-smart-contracts@latest
As the syntax of smart contracts works by inheritance, you can add underlying functionalities by importing predefined contracts. To create a new smart contract using LSPs, you can simply import
the standardized and modular presets from the previously installed @lukso/lsp-smart-contracts
library. These presets can then be combined to create complex contract deployments like Universal Profiles and Digital Assets.
As an example, you can import LSP7Mintable
to create a LSP7 contract that enables the contract owner to mint these tokens:
import { LSP7Mintable } from '@lukso/lsp-smart-contracts/contracts/LSP7DigitalAsset/presets/LSP7Mintable.sol';
// ...
contract MyToken is LSP7Mintable {
// your custom smart contract logic ...
}
After inheriting from LSPs, the contract might expect mandatory parameters to fullfil the standardization. In case of LSP7Mintable
, you must define a set of default parameters in the constructor of the smart contract, defined during the contracts deployment. You can find more information on the Create LSP7 Token Guide.
Compile and deploy contractsâ
After setting up the contract, you can compile it using the following command:
npx hardhat compile
Add the --verbose
and --show-stack-traces
flags for further information.
After the contract is compiled, you can create a deployment script to publish the contract on the blockchain.
- Deploy with Universal Profile
- Deploy with EOA
If you are deploying a contract as Universal Profile, you will have to prepare the payload of the contract deployment:
- Encode the constructor parameters
- Generate the full bytecode for the contract deployment
You can mimic calling the execute()
function on the Universal Profile using staticCall
. This address then matches the contract that will later be deployed using the same parameters.
import { ethers } from 'hardhat';
import * as dotenv from 'dotenv';
import LSP0Artifact from '@lukso/lsp-smart-contracts/artifacts/LSP0ERC725Account.json';
// Load environment variables
dotenv.config();
async function deployContract() {
// UP controller used for deployment
const [deployer] = await ethers.getSigners();
console.log('Deploying contract with Universal Profile controller: ', deployer.address);
// Load the Universal Profile
const universalProfile = await ethers.getContractAtFromArtifact(
LSP0Artifact,
process.env.UP_ADDR as string,
);
// Create custom bytecode for the contract deployment
const contractBytecode = (await ethers.getContractFactory('MyCustomContract')).bytecode;
const abiEncoder = new ethers.AbiCoder();
// Encode constructor parameters
const encodedConstructorParams = abiEncoder.encode(
[
// your custom constructor types
],
[
// your custom constructor parameters
],
);
// Add the constructor parameters to the contract bytecode
const contractBytecodeWithConstructor = ethers.concat([contractBytecode, encodedConstructorParams]);
// Get the address of the custom contract that will be created
const contractAddress = await universalProfile.execute.staticCall(
1, // Operation type: CREATE
ethers.ZeroAddress, // Target: 0x0 as contract will be initialized
0, // Value is empty
contractBytecodeWithConstructor, // Payload of the contract
);
// Deploy the contract by the Universal Profile
const tx = await universalProfile.execute(
1, // Operation type: CREATE
ethers.ZeroAddress, // Target: 0x0 as contract will be initialized
0, // Value is empty
contractBytecodeWithConstructor, // Payload of the contract
);
// Wait for the transaction to be included in a block
await tx.wait();
console.log('Contract deployed at: ', contractAddress);
}
deployContract()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
import { ethers } from 'hardhat';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config();
async function deployContract() {
// Signer used for deployment
const [deployer] = await ethers.getSigners();
console.log('Deploying contract with EOA: ', deployer.address);
// Deploy the contract with custom constructor parameters
const myCustomContract = await ethers.deployContract('MyCustomContract', [
// your custom constructor parameters
]);
// Wait for the transaction to be included in a block
await myCustomContract.waitForDeployment();
const contractAddress = await myCustomContract.getAddress();
console.log('Contract deployed at: ', contractAddress);
}
deployContract()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
You can then continue to run the deployment script:
npx hardhat --network luksoTestnet run scripts/deployMyCustomToken.ts
You can find ready-to-go example contracts within the lukso-playground
repository.
Please remember to always be careful when deploying smart contracts. Do not forget to audit your code before deploying your smart contracts in production.
Verify smart contractsâ
To verify a deployed contract, you can use the Blockscout API properties set up within the hardhat.config.ts
. You have to pass a file with all the constructor arguments that you've defined when deploying the smart contract, as well as the address of the deployed smart contract. The parameters and the compiled contract code are then used to verify the payload of the deployed contract on the address.
Create a new file that houses all the constructor parameters upon deployment:
module.exports = [
// your custom constructor parameters ...
];
You can run the verification script as on the network using the following command:
npx hardhat verify <MyContractAddress> --constructor-args ./verify/myContractParameters.ts --network luksoTestnet