Skip to main content

Writing Apps on SOCKET

1. Introduction

In this guide, we’ll build a Airdrop application using the SOCKET Protocol. Make sure your environment is set up with socket-starter-kit before proceeding.

You’ll learn how to:

  • Create a multi-chain mintable and burnable token contract.
  • Build an airdrop mechanism.
  • Use offchainVM to check eligibility and trigger on-chain minting.
  • Deploy and test your app across multiple chains.

2. Architecture Overview

The System consists of 3 main components.

  • An on chain ERC20 Token Contract that can be deployed to any chain.
  • A Deployer Contract on offchainVM to deploy the ERC20 Token instances.
  • An AppGateway Contract on offchainVM that verifies users and triggers minting of tokens on desired instance.

Deployment Flow

deployment_flow.png

Mint flow

mint_flow.png

3. Step-by-Step Implementation

To begin, we’ll implement a token contract for our application. The token will be an ERC20 token with mint and burn capabilities, specifically designed to interact with the SOCKET Protocol. We'll use Solady, a lightweight library for ERC20 implementation.

Install Solady

Install Solady as a dependency using Forge:

forge install vectorized/solady

Token Contract Implementation: MyToken.sol

Here’s the implementation of the MyToken contract that uses Solmate's ERC20 as a base. Apart from the standard ERC20 functionality, it has functions to mint and burn tokens from the AppGateway.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

import "solady/tokens/ERC20.sol";

contract MyToken is ERC20 {
string private _name;
string private _symbol;
uint8 private _decimals;

address public _SOCKET;

constructor(string memory name_, string memory symbol_, uint8 decimals_) {
_name = name_;
_symbol = symbol_;
_decimals = decimals_;
_SOCKET = msg.sender;
}

error NotSOCKET();

modifier onlySOCKET() {
if (msg.sender != _SOCKET) revert NotSOCKET();
_;
}

function mint(address to_, uint256 amount_) external onlySOCKET {
_mint(to_, amount_);
}

function burn(uint256 amount_) external onlySOCKET {
_burn(msg.sender, amount_);
}

function name() public view override returns (string memory) {
return _name;
}

function symbol() public view override returns (string memory) {
return _symbol;
}

function decimals() public view override returns (uint8) {
return _decimals;
}
}

This contract is expected to be deployed via SOCKET, therefore _SOCKET address is set as the msg.sender in the constructor.

The mint and burn functions have onlySOCKET modifier because these are called by AppGateway via SOCKET.

Deployer Contract Implementation: MyTokenDeployer.sol

Here’s the implementation of MyTokenDeployer contract which will be deployed to offchainVM. It extends the AppDeployerBase to manage the deployment process.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

import "./MyToken.sol";
import "socket-protocol/contracts/base/AppDeployerBase.sol";

contract MyTokenDeployer is AppDeployerBase {
bytes32 public myToken = _createContractId("myToken");

constructor(
address addressResolver_,
FeesData memory feesData_,
string memory name_,
string memory symbol_,
uint8 decimals_
) AppDeployerBase(addressResolver_) {
creationCodeWithArgs[myToken] = abi.encodePacked(
type(MyToken).creationCode,
abi.encode(name_, symbol_, decimals_)
);
_setFeesData(feesData_);
}

function deployContracts(uint32 chainSlug) external async {
_deploy(myToken, chainSlug);
}

function initialize(uint32 chainSlug) public override async {}
}

To identify the contract, we use a bytes32 variable. This is a unique identifier for the contract and is used to fetch the creationCode, on-chain addresses and forwarder addresses from maps in AppGatewayBase. This identifier can be created using _createContractId function.

In the constructor, MyToken's creationCode with constructor parameters is stored in a mapping. This stored code is used for deploying the token to the underlying chains. While this example handles a single contract, you can extend it to manage multiple contracts by storing their creation codes. The constructor also takes in addressResolver and feesData, we will talk more on these at a later stage. Or you can read more about them here.

The deployContracts function takes a chainSlug as an argument, specifying the chain where the contract should be deployed. It calls the inherited _deploy function and uses the async modifier for interacting with underlying chains.

The initialize function is empty in this example. Use it for setting chain-specific or dynamic variables after deployment if needed. More details here.

AppGateway Contract implementation: MyTokenAppGateway.sol

MyTokenAppGateway is an AppGateway, it extends AppGatewayBase for logic related to interacting with onchain instances. This is where users interact with your app without worrying about the underlying chains. It has an addAirdropReceivers function that the owner can call and a claimAirdrop function that users can call.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

import "socket-protocol/contracts/base/AppGatewayBase.sol";
import "solady/auth/Ownable.sol";
import "./MyToken.sol";

contract MyTokenAppGateway is AppGatewayBase, Ownable {
mapping(address => uint256) public airdropReceivers;

constructor(
address _addressResolver,
address deployerContract_,
FeesData memory feesData_
) AppGatewayBase(_addressResolver) Ownable() {
addressResolver.setContractsToGateways(deployerContract_);
_setFeesData(feesData_);
}

function addAirdropReceivers(
address[] calldata receivers_,
uint256[] calldata amounts_
) external onlyOwner {
for (uint256 i = 0; i < receivers_.length; i++) {
airdropReceivers[receivers_[i]] = amounts_[i];
}
}

function claimAirdrop(address _instance) external async {
uint256 amount = airdropReceivers[msg.sender];
airdropReceivers[msg.sender] = 0;
MyToken(_instance).mint(msg.sender, amount);
}
}

In constructor we set the deployerContract as a contract that belongs to this Gateway. This is how you indicate which contracts are allowed to call your onchain contracts and SOCKET protocol knows where to charge fees from when a contract on offchainVM calls a contract on chain.

The claimAirdrop function again has an async modifier, similar to deployContracts and initialize function of the deployer. This modifier should be used whenever read or write calls are made to the underlying contracts. You can read more about writes and reads.

4. Deployment and Fee setup

With the contracts ready, we can go on to deploy things. In true Chain Abstracted spirit, you as a developer only need to interact with the offchainVM and never with the chains directly unless you want to verify if things were done correctly.

Deploy Contracts to offchainVM: SetupMyToken.s.sol

You need to deploy the MyTokenDeployer and MyTokenDistrbutor to the offchainVM.

You can get the addressResolver from here.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/Console.sol";
import {MyTokenAppGateway} from "../src/MyTokenAppGateway.sol";
import {MyTokenDeployer} from "../src/MyTokenDeployer.sol";
import {FeesData} from "lib/socket-protocol/contracts/common/Structs.sol";
import {ETH_ADDRESS} from "lib/socket-protocol/contracts/common/Constants.sol";

contract SetupMyToken is Script {
function run() public {
address addressResolver = vm.envAddress("ADDRESS_RESOLVER");

string memory rpc = vm.envString("SOCKET_RPC");
vm.createSelectFork(rpc);

uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

// Setting fee payment on Ethereum Sepolia
FeesData memory feesData = FeesData({
feePoolChain: 11155111,
feePoolToken: ETH_ADDRESS,
maxFees: 0.01 ether
});

MyTokenDeployer myTokenDeployer = new MyTokenDeployer(
addressResolver,
feesData,
"MyToken",
"MTK",
18
);

MyTokenAppGateway myTokenAppGateway = new MyTokenAppGateway(
addressResolver,
address(myTokenDeployer),
feesData
);

console.log("MyTokenDeployer: ", address(myTokenDeployer));
console.log("MyTokenAppGateway: ", address(myTokenAppGateway));
}
}

Run the script using cast, providing rpc and private key.

forge script script/SetupMyToken.s.sol --broadcast

Fund your App

Next, go on to setup fees so that offchainVM can send transactions and deploy contracts on your app’s behalf. On any supported chain, deposit fees against MyTokenAppGateway’s address. Read more about setting up fees and generating feesData here.

Deploy Token to chains: DeployMyToken.s.sol

Once your app is funded, you can trigger the deployment of MyToken on desired chains. In this case as well, just interact with offchainVM. Call deployContracts function of MyTokenDeployer contract for each chain where deployment needs to be done.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {MyTokenDeployer} from "../src/MyTokenDeployer.sol";

contract DeployMyToken is Script {
function run() public {
string memory rpc = vm.envString("SOCKET_RPC");
vm.createSelectFork(rpc);

uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

MyTokenDeployer myTokenDeployer = MyTokenDeployer(<deployerAddress>);
myTokenDeployer.deployContracts(<chainSlug1>);
myTokenDeployer.deployContracts(<chainSlug2>);
myTokenDeployer.deployContracts(<chainSlug3>);
}
}

Set proper values for deployerAddress and chainSlugs before running this script.

deployerAddress should have been logged in by previous script.

forge script ./script/DeployMyToken.s.sol --broadcast

Deployment of on chain contracts should take couple minutes. You can track the status of this request and also check the deployed addresses using our apis.

5. Testing

Add Airdrop Receivers: AddReceivers.s.sol

Once the setup is done, you can call addAirdropReceivers.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {MyTokenAppGateway} from "../src/MyTokenAppGateway.sol";

contract AddReceivers is Script {
address[] receivers = [
<receiver1>,
<receiver2>,
<receiver3>
];
uint256[] amounts = [
<amount1>,
<amount2>,
<amount3>
];

function run() public {
string memory rpc = vm.envString("SOCKET_RPC");
vm.createSelectFork(rpc);

uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

MyTokenAppGateway myTokenAppGateway = MyTokenAppGateway(<myTokenAppGatewayAddress>);
myTokenAppGateway.addAirdropReceivers(receivers, amounts);
}
}

Claim Airdrop: ClaimAirdrop.s.sol

For each receiver that was added in previous step, they can call claimAirdrop with their desired instance address to mint tokens on the desired chain. Use our apis to get instance addresses.

Note that the instance addresses are not the same as where token contracts are deployed on chain. The instance here is a forwarder address, read more about it here.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {MyTokenAppGateway} from "../src/MyTokenAppGateway.sol";

contract ClaimAirdrop is Script {
function run() public {
string memory rpc = vm.envString("SOCKET_RPC");
vm.createSelectFork(rpc);

uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

MyTokenAppGateway myTokenAppGateway = MyTokenAppGateway(<myTokenAppGatewayAddress>);
myTokenAppGateway.claimAirdrop(<instance>);
}
}