NEON — Solana’s EVM

Smith | MCF
6 min readNov 20, 2023

Solana has its own EVM called Neon, the idea is you can deploy your Solidity contracts on Solana for a fraction of the cost of Ethereum and ten times the speed. I thought I’d take a look. In this tutorial I will compile and deploy a very simple smart contract on Neon mainnet and build a web3 frontend that interacts with it.

If you want to play along, you will need a browser with MetaMask installed and switched to the Neon mainnet, plus some Neon tokens. The code is just 2 files, the simple.sol contract and the index.html frontend. The frontend can be seen here on Arweave.

Add Neon to MetaMask

Begin by adding Neon mainnet to your MetaMask. By default, Metamask will be pointing to Ethereum Mainnet, switch to the Neon Mainnet by opening this webpage and clicking “Connect Wallet” — this will prompt you to add the Neon network to MetaMask and switch to it. Be safe and never use an online wallet with a lot of funds, keep the big money on Ledger or a paper wallet.

The easiest way to compile, debug and deploy Solidity contracts is via remix, go to Home, click the NewFile button, give the file a name, “simple.sol” for example, then paste in your contract, in my case it was the following:

// SPDX-License-Identifier: ABC
pragma solidity ^0.8.21;

contract Simple {
uint public counter=1;

function getCounter() public view returns (uint) {
return counter;
}

function incCounter() public {
counter++;
}
}

This contract does nothing except store a counter on chain, and allow a user to increment it by 1.

Switch to the Compile side tab, compile the contract. Then switch to the Deploy side tab, select “Injected Provider” from the environment dropdown, this will use Metamask to determine the EVM chain to connect to. Deploy the contract, as we are working on Mainnet, this will require some Neon tokens. It cost me 5 Neon Tokens to deploy the contract, cheap but not quite as cheap as I hoped for.

Under “Deployed Contracts” you will now see your contract and an address, click the Copy symbol and observe the contract on the chain using the Neon Explorer. You will note that the Contract Balance is zero, on the Transactions tab you will see the txhash and the time of creation, on the Contract tab you will see the EVM bytecode. Save the contract address for later.

Note: If the create contract transaction takes too long to be mined, you need to use the NeonScan explorer to query the contract creation transaction, find the new contract address and add it manually.

Create a webpage that interacts with the Contract

We will make a single html file called index.html, some of this is just divs and buttons, I won’t talk about that, but here I briefly describe the relevant bits.

In the head section import the web3 library, this is an Ethereum library, because we basically are doing Ethereum, the Solana stuff is under-the-hood.

<script src="https://cdn.jsdelivr.net/npm/web3@1.10.2/dist/web3.min.js"></script>

Still on the remix website, switch back to the Compile tab and click the “Copy ABI” button. If you paste the contents, you will see a description of the Contract interface, we need this to programmatically interact with the contract.

The ABI + the address of the contract on chain are all that is required to interact with the contract. We can just paste them straight into the javascript part of the html file as constants, then create the contract object after initializing web3.

const contractABI = "paste the ABI here"
const contractAddress = "0x9561d9B6FB6Cbba4aADc39b0820Fb43Ea61bf494"

We create the contract object on page load:

  window.addEventListener('load', function () {
window.web3 = new Web3()
contract = new web3.eth.Contract(contractABI, contractAddress);
...

If you have MetaMask installed, an “ethereum” object will be injected into the browser window, this includes a “provider” (basically the RPC node MetaMask is using) allowing us to query the chain. This is the code that gets the counter value from the contract:

    const callData = await contract.methods.getCounter().encodeABI()
ethereum
.request({
method: 'eth_call',
params: [
{
to: contractAddress,
data: callData,
},
],
})
.then((counterValueHex) => {
const counterValue = window.web3.eth.abi.decodeParameter("uint256", counterValueHex)
document.getElementById("counter").innerHTML = "Current Counter Value: "+counterValue
})
.catch((error) => {
console.log("getCounter() failed -- " + error.message)
})

The line with encodeABI() encodes the contract call to getCounter() in a way the node will understand. The call could include parameters, but in this case it does not. We pass the contract address with the call data via the “ethereum” object to the MetaMask provider. After a short time, we are returned (hopefully) the counter value in hex, we decode it and place it on the webpage.

The code contains a similar call to get the current block of the Neon chain, I won’t explain it, but you can understand it easily.

Note: Metamask won’t inject the “ethereum” object if the page is served as file:/// but you can get round this by running http-server in the directory containing your html file, then navigating to the external url shown by http-server on the console. Warning, you are now serving the web page to the world.

Note: If the page is not served https, you will get the “SubtleCrypto” error, this doesn’t matter for our purposes.

So far, we have only read the chain, if we want to change the state of the contract, we will need to make a transaction. For this the user will need to spend funds (gas), so we need not only access to MetaMask, but also the user’s account.

Getting the user’s account is a permissioned step, I have a button that when clicked executes the following code, which pops up MetaMask requesting access to the account. If the user allows it, we will have the user’s account.

  var accountAddress       
async function getAccount() {
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
.catch((err) => {
if (err.code === 4001) {
// EIP-1193 userRejectedRequest error
console.log('User said no!')
} else {
console.error(err)
}
})
accountAddress = accounts[0]
document.getElementById("account").innerHTML = "Account: "+accountAddress
...
}
...
<button id="connect" onclick="getAccount()">Connect MetaMask</button>

Once the user has allowed us access to the account we can create a transaction. This again is a permissioned step that the user has to confirm. The following code does that:

    const callData = await contract.methods.incCounter().encodeABI()
ethereum
.request({
method: 'eth_sendTransaction',
params: [
{
type: 0x0,
from: accountAddress,
to: contractAddress,
value: 0,
data: callData,
},
],
})
.then((txHash) => console.log(txHash))
.catch((error) => console.error(error))

Note: Neon doesn’t support EIP-1559 transactions yet, so we need to submit a legacy transaction, adding the parameter field “type: 0x0” makes that happen.

When the user clicks on the button and confirms in MetaMask the transaction is signed and sent to the network, the contract call is made and the counter is incremented. For me it cost 0.0128 Neon Tokens, pretty cheap really.

That’s it for the tutorial.

Now the not-so-happy part… I used to develop Solidity contracts for Ethereum, so I initially had more ambitious plans for this little tutorial, but as soon as it started getting more complex I ran into problems. A transaction calling a payable function with several parameters and some interesting logic failed, now with Ethereum you get a lot of clues as to why, but because Neon is really running on Solana (one Neon call creates multiple Solana transactions under-the-hood) it’s hard to know why. You can see the Solana transactions that failed, but those are far removed from what you submitted, so that doesn’t help. It may well be that I was doing something wrong, but how to know? 🤷 If Neon can make debugging easier (and maybe have a bit more support on the developer discord) I will definitely try again.

Our Solana Validator

--

--