Skip to main content

How to Create a Staking Vault Smart Contract with ApeWorX/ape

Updated on
Sep 4, 2024

21 min read

tip

While this guide demonstrates deployment on the Ethereum Sepolia testnet, the smart contract code is compatible with any EVM-based blockchain. Feel free to adapt the deployment process for other networks like Base, Optimism, Avalanche, and more. See all the chains QuickNode supports here.

Please note that the code presented in this guide is intended for educational purposes and as a starting point for development. It has not been thoroughly tested for production use. If you're interested in the Solidity version, check out this guide.

Overview​

ApeWorX, or simply "ape", is a powerful and flexible development framework for smart contract development on EVM-compatible blockchains. In this guide, we'll create and deploy an ERC4626-compliant staking vault smart contract written in Vyper and test it using the ApeWorX framework.

Let's get started!

What You Will Need​


DependencyVersion
Python^3.9.2
pip24.2
eth-ape0.8.10

What You Will Do​


  • Learn about ApeWorX/ape
  • Create and deploy an ERC-20 token using Ape
  • Create and deploy an ERC-4626 staking vault contract to stake an ERC-20 token
  • Test depositing and withdrawing shares on the staking vault smart contract

What is Ape (ApeWorX)​

Ape is a Python-based development framework and smart contract tool suite for the Ethereum ecosystem. It provides a comprehensive set of tools for writing, testing, and deploying smart contracts. Ape supports multiple languages for smart contract development, including Solidity and Vyper. This guide will focus on creating smart contracts with Vyper, a contract-oriented, pythonic programming language that targets the Ethereum Virtual Machine (EVM). Vyper is designed to be more secure and easier to audit than Solidity, making it an excellent choice for new developers.

Set Up Project​

First, let's set up our QuickNode endpoint so we have way to communicate with the Ethereum network. After, we'll initialize an Ape project directory, then install the necessary dependencies.

Create a QuickNode RPC Endpoint​

To interact with the Ethereum network and deploy the smart contracts for this guide, we'll need an RPC endpoint. Follow these steps to create one using QuickNode:


  1. Sign up for an account at QuickNode
  2. Create a new endpoint for Ethereum mainnet
  3. Keep the HTTP Provider URL handy for later use

Initialize Ape Project​

Create a project directory with the following commands:

mkdir staking-vault-ape
cd staking-vault-ape

It's a good practice to setup a virtual environment when using ape. Set one up with the following commands below:

python -m venv vyper-env

Depending on how your Python installation is configured, you may have to use python3

Then activate it:

source vyper-env/bin/activate

Next, install Ape:

pip install eth-ape

Then, initialize a new Ape project:

ape init

When prompted for a project name, input "staking-vault-ape". When initialized you'll see the following directory structure:

β”œβ”€β”€ contracts // Used for Solidity and Vyper smart contracts
β”œβ”€β”€ scripts // Scripts for deployment and smart contract interaction
β”œβ”€β”€ tests // Local testing
β”œβ”€β”€ vyper-env

Then, if you don't have Vyper installed already, install it via the ape plugin:

ape plugins install vyper

Next, open the project in a code editor and update the ape-config.yaml in your project directory to include your QuickNode HTTP Provider URL endpoint in the YOUR_QUICKNODE_RPC_URL placeholder.

name: ape-staking-vault

node:
ethereum:
sepolia:
uri: YOUR_QUICKNODE_RPC_URL

Remember to save the file.

Next, let's configure the account we'll use for smart contract deployment. Run the command below in your terminal and follow the prompts (have your private key handy):

ape accounts import my_account 

my_account is an alias and can be modified to your liking. You can also check out other Account options methods here.

You will be prompted to input your private key along with a password.

In the next section, we'll set up the smart contracts and scripts needed to create, deploy and test the staking reward smart contracts.

Create an ERC-20 Token​

Let's create a simple ERC-20 token that we'll use for staking. The token will be named StakingToken (ST) and upon contract deployment will mint 1000000 tokens to the contract deployer.

Create a new file contracts/StakingToken.vy and add the following Vyper code below:

# @version 0.3.7

from vyper.interfaces import ERC20

implements: ERC20

# ERC20 Token Metadata
NAME: constant(String[20]) = "StakingToken"
SYMBOL: constant(String[5]) = "ST"
DECIMALS: constant(uint8) = 18

# ERC20 State Variables
totalSupply: public(uint256)
balanceOf: public(HashMap[address, uint256])
allowance: public(HashMap[address, HashMap[address, uint256]])

# Events
event Transfer:
sender: indexed(address)
receiver: indexed(address)
amount: uint256

event Approval:
owner: indexed(address)
spender: indexed(address)
amount: uint256

owner: public(address)
isMinter: public(HashMap[address, bool])
nonces: public(HashMap[address, uint256])
DOMAIN_SEPARATOR: public(bytes32)
DOMAIN_TYPE_HASH: constant(bytes32) = keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
PERMIT_TYPE_HASH: constant(bytes32) = keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)')

@external
def __init__():
self.owner = msg.sender
self.totalSupply = 1000000
self.balanceOf[msg.sender] = 1000000
self.DOMAIN_SEPARATOR = keccak256(
concat(
DOMAIN_TYPE_HASH,
keccak256(NAME),
keccak256("1.0"),
_abi_encode(chain.id, self)
)
)

@pure
@external
def name() -> String[20]:
return NAME

@pure
@external
def symbol() -> String[5]:
return SYMBOL

@pure
@external
def decimals() -> uint8:
return DECIMALS

@external
def transfer(receiver: address, amount: uint256) -> bool:
assert receiver not in [empty(address), self]

self.balanceOf[msg.sender] -= amount
self.balanceOf[receiver] += amount

log Transfer(msg.sender, receiver, amount)
return True

@external
def transferFrom(sender: address, receiver: address, amount: uint256) -> bool:
assert receiver not in [empty(address), self]

self.allowance[sender][msg.sender] -= amount
self.balanceOf[sender] -= amount
self.balanceOf[receiver] += amount

log Transfer(sender, receiver, amount)
return True

@external
def approve(spender: address, amount: uint256) -> bool:
"""
@param spender The address that will execute on owner behalf.
@param amount The amount of token to be transferred.
"""
self.allowance[msg.sender][spender] = amount

log Approval(msg.sender, spender, amount)
return True

@external
def burn(amount: uint256) -> bool:
"""
@notice Burns the supplied amount of tokens from the sender's wallet.
@param amount The amount of token to be burned.
"""
self.balanceOf[msg.sender] -= amount
self.totalSupply -= amount

log Transfer(msg.sender, empty(address), amount)

return True

@external
def mint(receiver: address, amount: uint256) -> bool:
"""
@notice Function to mint tokens
@param receiver The address that will receive the minted tokens.
@param amount The amount of tokens to mint.
@return A boolean that indicates if the operation was successful.
"""
assert msg.sender == self.owner or self.isMinter[msg.sender], "Access is denied."
assert receiver not in [empty(address), self]

self.totalSupply += amount
self.balanceOf[receiver] += amount

log Transfer(empty(address), receiver, amount)

return True

@external
def permit(owner: address, spender: address, amount: uint256, expiry: uint256, signature: Bytes[65]) -> bool:
"""
@notice
Approves spender by owner's signature to expend owner's tokens.
See https://eips.ethereum.org/EIPS/eip-2612.
@param owner The address which is a source of funds and has signed the Permit.
@param spender The address which is allowed to spend the funds.
@param amount The amount of tokens to be spent.
@param expiry The timestamp after which the Permit is no longer valid.
@param signature A valid secp256k1 signature of Permit by owner encoded as r, s, v.
@return True, if transaction completes successfully
"""
assert owner != empty(address) # dev: invalid owner
assert expiry == 0 or expiry >= block.timestamp # dev: permit expired
nonce: uint256 = self.nonces[owner]
digest: bytes32 = keccak256(
concat(
b'\x19\x01',
self.DOMAIN_SEPARATOR,
keccak256(
_abi_encode(
PERMIT_TYPE_HASH,
owner,
spender,
amount,
nonce,
expiry,
)
)
)
)
# NOTE: signature is packed as r, s, v
r: uint256 = convert(slice(signature, 0, 32), uint256)
s: uint256 = convert(slice(signature, 32, 32), uint256)
v: uint256 = convert(slice(signature, 64, 1), uint256)
assert ecrecover(digest, v, r, s) == owner # dev: invalid signature
self.allowance[owner][spender] = amount
self.nonces[owner] = nonce + 1

log Approval(owner, spender, amount)
return True

The code above is a compliant ERC-20 token. You can see the EIP standard here, or check out our guide for more info: How to Create and Deploy an ERC20 Token

Create an ERC4626 Staking Vault Contract​

For this guide, we'll implement an ERC4626-compliant staking vault. ERC4626 standardizes the interface for tokenized vaults, making it easier to interact with and integrate into other DeFi protocols. Our contract will allow users to deposit an ERC20 token (the asset) and receive shares in return, representing their stake in the vault.

Now, create a new file called contracts/StakingVault.vy and input the code below:

# @version 0.3.7
from vyper.interfaces import ERC20
from vyper.interfaces import ERC4626

implements: ERC20
implements: ERC4626

##### ERC20 #####

totalSupply: public(uint256)
balanceOf: public(HashMap[address, uint256])
allowance: public(HashMap[address, HashMap[address, uint256]])

NAME: constant(String[10]) = "Test Vault"
SYMBOL: constant(String[5]) = "vTEST"
DECIMALS: constant(uint8) = 18

event Transfer:
sender: indexed(address)
receiver: indexed(address)
amount: uint256

event Approval:
owner: indexed(address)
spender: indexed(address)
allowance: uint256

##### ERC4626 #####

asset: public(ERC20)

event Deposit:
depositor: indexed(address)
receiver: indexed(address)
assets: uint256
shares: uint256

event Withdraw:
withdrawer: indexed(address)
receiver: indexed(address)
owner: indexed(address)
assets: uint256
shares: uint256


@external
def __init__(asset: ERC20):
self.asset = asset


@view
@external
def name() -> String[10]:
return NAME


@view
@external
def symbol() -> String[5]:
return SYMBOL


@view
@external
def decimals() -> uint8:
return DECIMALS


@external
def transfer(receiver: address, amount: uint256) -> bool:
self.balanceOf[msg.sender] -= amount
self.balanceOf[receiver] += amount
log Transfer(msg.sender, receiver, amount)
return True


@external
def approve(spender: address, amount: uint256) -> bool:
self.allowance[msg.sender][spender] = amount
log Approval(msg.sender, spender, amount)
return True


@external
def transferFrom(sender: address, receiver: address, amount: uint256) -> bool:
self.allowance[sender][msg.sender] -= amount
self.balanceOf[sender] -= amount
self.balanceOf[receiver] += amount
log Transfer(sender, receiver, amount)
return True


@view
@external
def totalAssets() -> uint256:
return self.asset.balanceOf(self)


@view
@internal
def _convertToAssets(shareAmount: uint256) -> uint256:
totalSupply: uint256 = self.totalSupply
if totalSupply == 0:
return 0

# NOTE: `shareAmount = 0` is extremely rare case, not optimizing for it
# NOTE: `totalAssets = 0` is extremely rare case, not optimizing for it
return shareAmount * self.asset.balanceOf(self) / totalSupply


@view
@external
def convertToAssets(shareAmount: uint256) -> uint256:
return self._convertToAssets(shareAmount)


@view
@internal
def _convertToShares(assetAmount: uint256) -> uint256:
totalSupply: uint256 = self.totalSupply
totalAssets: uint256 = self.asset.balanceOf(self)
if totalAssets == 0 or totalSupply == 0:
return assetAmount # 1:1 price

# NOTE: `assetAmount = 0` is extremely rare case, not optimizing for it
return assetAmount * totalSupply / totalAssets


@view
@external
def convertToShares(assetAmount: uint256) -> uint256:
return self._convertToShares(assetAmount)


@view
@external
def maxDeposit(owner: address) -> uint256:
return MAX_UINT256


@view
@external
def previewDeposit(assets: uint256) -> uint256:
return self._convertToShares(assets)


@external
def deposit(assets: uint256, receiver: address=msg.sender) -> uint256:
shares: uint256 = self._convertToShares(assets)
self.asset.transferFrom(msg.sender, self, assets)

self.totalSupply += shares
self.balanceOf[receiver] += shares
log Transfer(empty(address), receiver, shares)
log Deposit(msg.sender, receiver, assets, shares)
return shares


@view
@external
def maxMint(owner: address) -> uint256:
return MAX_UINT256


@view
@external
def previewMint(shares: uint256) -> uint256:
assets: uint256 = self._convertToAssets(shares)

# NOTE: Vyper does lazy eval on if, so this avoids SLOADs most of the time
if assets == 0 and self.asset.balanceOf(self) == 0:
return shares # NOTE: Assume 1:1 price if nothing deposited yet

return assets


@external
def mint(shares: uint256, receiver: address=msg.sender) -> uint256:
assets: uint256 = self._convertToAssets(shares)

if assets == 0 and self.asset.balanceOf(self) == 0:
assets = shares # NOTE: Assume 1:1 price if nothing deposited yet

self.asset.transferFrom(msg.sender, self, assets)

self.totalSupply += shares
self.balanceOf[receiver] += shares
log Transfer(empty(address), receiver, shares)
log Deposit(msg.sender, receiver, assets, shares)
return assets


@view
@external
def maxWithdraw(owner: address) -> uint256:
return MAX_UINT256 # real max is `self.asset.balanceOf(self)`


@view
@external
def previewWithdraw(assets: uint256) -> uint256:
shares: uint256 = self._convertToShares(assets)

# NOTE: Vyper does lazy eval on if, so this avoids SLOADs most of the time
if shares == assets and self.totalSupply == 0:
return 0 # NOTE: Nothing to redeem

return shares


@external
def withdraw(assets: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256:
shares: uint256 = self._convertToShares(assets)

# NOTE: Vyper does lazy eval on if, so this avoids SLOADs most of the time
if shares == assets and self.totalSupply == 0:
raise # Nothing to redeem

if owner != msg.sender:
self.allowance[owner][msg.sender] -= shares

self.totalSupply -= shares
self.balanceOf[owner] -= shares

self.asset.transfer(receiver, assets)
log Transfer(owner, empty(address), shares)
log Withdraw(msg.sender, receiver, owner, assets, shares)
return shares


@view
@external
def maxRedeem(owner: address) -> uint256:
return MAX_UINT256 # real max is `self.totalSupply`


@view
@external
def previewRedeem(shares: uint256) -> uint256:
return self._convertToAssets(shares)


@external
def redeem(shares: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256:
if owner != msg.sender:
self.allowance[owner][msg.sender] -= shares

assets: uint256 = self._convertToAssets(shares)
self.totalSupply -= shares
self.balanceOf[owner] -= shares

self.asset.transfer(receiver, assets)
log Transfer(owner, empty(address), shares)
log Withdraw(msg.sender, receiver, owner, assets, shares)
return assets

The code above is compliant to the ERC4626 standard and also inherits the ERC-20 standard. Functions implemented include:


  • asset(): Returns the address of the underlying asset token
  • totalAssets(): Returns the total amount of underlying assets held by the vault
  • convertToShares(assets): Calculates how many shares would be minted for a given amount of assets
  • convertToAssets(shares): Calculates how many assets would be redeemed for a given amount of shares
  • maxDeposit(receiver): Returns the maximum amount of assets that can be deposited
  • previewDeposit(assets): Simulates the amount of shares that would be minted for a deposit
  • deposit(assets, receiver):Deposits assets into the vault and mints shares to the receiver
  • maxMint(receiver): Returns the maximum amount of shares that can be minted
  • previewMint(shares):Simulates the amount of assets needed to mint a specific amount of shares
  • mint(shares, receiver):Mints a specific amount of shares to the receiver, depositing the necessary assets
  • maxWithdraw(owner):Returns the maximum amount of assets that can be withdrawn by the owner
  • previewWithdraw(assets):Simulates the amount of shares that would be burned for a withdrawal
  • withdraw(assets, receiver, owner): Withdraws a specific amount of assets to the receiver, burning the necessary shares
  • maxRedeem(owner): Returns the maximum amount of shares that can be redeemed by the owner
  • previewRedeem(shares): Simulates the amount of assets that would be withdrawn for a redemption
  • redeem(shares, receiver, owner): Redeems a specific amount of shares, withdrawing assets to the receiver

For more information on this standard, check out the EIP here. Alternatively, you can also check out our guide: How to Use ERC-4626 with Your Smart Contract

In the next section, we'll test depositing and withdrawing tokens from the vault contract before deploying to a public testnet like Sepolia.

Testing Depositing and Withdrawing​

To test our ERC4626 vault contract, we'll create a Python script using Ape's testing framework. This will allow us to deploy our contracts, deposit tokens (stake), and withdraw tokens (unstake) in a local test environment, ensuring our implementation adheres to the ERC4626 standard.

Create a new file in your project directory called tests/test_vault.py and add the following code:

import pytest
from ape import accounts, project
from ape.exceptions import VirtualMachineError

@pytest.fixture(scope="module")
def owner(accounts):
return accounts[0]

@pytest.fixture(scope="module")
def user1(accounts):
return accounts[1]

@pytest.fixture(scope="module")
def user2(accounts):
return accounts[2]

@pytest.fixture(scope="function")
def asset_token(owner, project):
# Deploy a mock ERC20 token to act as the asset
return owner.deploy(project.StakingToken)

@pytest.fixture(scope="function")
def staking_reward(owner, asset_token, project):
# Deploy the StakingVault contract
return owner.deploy(project.StakingVault, asset_token.address)

def test_initial_state(staking_reward, asset_token):
assert staking_reward.name() == "Test Vault"
assert staking_reward.symbol() == "vTEST"
assert staking_reward.decimals() == 18
assert staking_reward.totalSupply() == 0
assert staking_reward.totalAssets() == 0
assert staking_reward.asset() == asset_token.address

def test_deposit_and_withdraw(staking_reward, asset_token, owner, user1):
# Mint some tokens to user1 for testing
asset_token.mint(user1, 1000, sender=owner)

# Approve the staking contract to spend user1's tokens
asset_token.approve(staking_reward.address, 500, sender=user1)

# Deposit tokens
staking_reward.deposit(500, user1, sender=user1)

assert staking_reward.balanceOf(user1) == 500
assert asset_token.balanceOf(staking_reward.address) == 500

# Withdraw tokens
staking_reward.withdraw(500, user1, sender=user1)

assert staking_reward.balanceOf(user1) == 0
assert asset_token.balanceOf(user1) == 1000

def test_conversion_functions(staking_reward, asset_token, owner, user1):
# Mint and deposit tokens for conversion tests
asset_token.mint(user1, 1000, sender=owner)
asset_token.approve(staking_reward.address, 500, sender=user1)
staking_reward.deposit(500, user1, sender=user1)

# Test convertToAssets
shares = staking_reward.convertToShares(100)
assert shares == 100

# Test convertToShares
assets = staking_reward.convertToAssets(100)
assert assets == 100

To run these tests, use the following command in your terminal:

ape test

You'll see an output like:

tests/test_staking.py ...                                                                                                                                      [100%]

============================================================================= 3 passed in 2.68s ==============================================================================

Deployment​

Now that are tests are complete. Let's deploy the smart contracts to a testnet.

Create a scripts/deploy_token.py file and input the following code:

from ape import accounts, project, networks

def main():
# Connect to the Sepolia network
networks.parse_network_choice("ethereum:sepolia") # Use the network as defined in your ape-config.yaml

# Load the imported account
account = accounts.load("my_account")
print(f"Using account: {account.address}")

# Deploy the StakingToken contract
staking_token = account.deploy(project.StakingToken)

print(f"StakingToken deployed at: {staking_token.address}")

return staking_token

if __name__ == "__main__":
main()

Deploy it with the following terminal command:

ape run deploy_token --network ethereum:sepolia:node

You will be prompted to sign the transaction and enter your account password you previously set up.

Then, create a scripts/deploy_staking_vault.py file and input the code below:

from ape import accounts, project, networks, Contract

def main():
# Connect to the Sepolia network using our custom configuration
networks.parse_network_choice("ethereum:sepolia")

# Load the imported account
account = accounts.load("my_account")
print(f"Using account: {account.address}")

# Specify the asset token address to be used during initialization
asset_token_address = "YOUR_ERC20_TOKEN_ADDRESS" # Replace with your actual deployed address

# Load the existing asset token contract
asset_token = Contract(asset_token_address)
print(f"Loaded AssetToken at: {asset_token.address}")

# Print available contracts
print("Available contracts:", project.contracts)

# Deploy the StakingVault contract with the asset token address as argument
vault = account.deploy(project.StakingVault, asset_token_address)
print(f"StakingVault Vault deployed at: {vault.address}")

return asset_token, vault

if __name__ == "__main__":
main()

Remember to replace the YOUR_ERC20_TOKEN_ADDRESS with the token address outputted in your terminal from the previous step. This script above deploys our ERC4626-compliant staking vault, which will accept deposits of the asset token we deployed earlier.

Deploy it with the following terminal command:

ape run deploy_staking_vault --network ethereum:sepolia:node

You will be prompted again to sign the transaction. Once the transaction is confirmed, you will see an output like:

INFO: Connecting to existing Geth node at https://dry-omniscient-ensemble.ethereum-sepolia.quiknode.pro/[hidden].
Using account: 0x894bC5FB859CFD53E7E59E97a5CE3bf89D84fa04
Loaded AssetToken at: 0xCBF509F9E7251B6217A700EA7816Ce1a10894e48
Available contracts: <Contracts $HOME/Documents/Code/guide-tests/con-232-guide-idea-how-to-create-smart-contracts-with-apeworxape/staking-vault-ape/contracts>
DynamicFeeTransaction:
chainId: 11155111
from: 0x894bC5FB859CFD53E7E59E97a5CE3bf89D84fa04
gas: 930942
nonce: 44
value: 0
data: 0x307836...653438
type: 2
maxFeePerGas: 10571472034
maxPriorityFeePerGas: 420184473
accessList: []

Sign: [y/N]: y
Enter passphrase to unlock 'my_account' []:
Leave 'my_account' unlocked? [y/N]: y
INFO: Submitted https://sepolia.etherscan.io/tx/0xa3acdd62ba2d1f422663cdc617de541d19534cdbffd9f5dda6baf81eb602dc9c
Confirmations (2/2): 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2/2 [00:42<00:00, 21.45s/it]
INFO: Confirmed 0xa3acdd62ba2d1f422663cdc617de541d19534cdbffd9f5dda6baf81eb602dc9c (total fees paid = 7006047733668594)
INFO: Confirmed 0xa3acdd62ba2d1f422663cdc617de541d19534cdbffd9f5dda6baf81eb602dc9c (total fees paid = 7006047733668594)
SUCCESS: Contract 'StakingVault' deployed to: 0x73e78463952960200322D004dCb13602eB2F8468
StakingVault Vault deployed at: 0x73e78463952960200322D004dCb13602eB2F8468

That's it! Your contracts are now deployed on a public testnet. In the next section, we'll cover how you can make them open-source. However, before moving on check Etherscan to ensure they are not open-source already (if they are, you can skip the next section).

Verify on Etherscan​

To make your smart contracts open-source, you can verify them on a block explorer like Etherscan. Follow the steps below to do so:


  1. Navigate to your smart contract on https://etherscan.io (you will need to do this for both contracts; i.e., StakingToken, StakingVault)
  2. Go to the "Contract" tab and click "Verify and Publish"
  3. Compiler Type: Vyper
  4. Compiler Version: 0.3.7 # Change as needed
  5. No License

On the next page, enter the contract code. For the StakingToken there is no constructor arugments.

For the StakingVault contract, you will need to encode the constructor arugments. Create a encode.py file in the same project directory and include the code below.

from eth_abi import encode

# Values from your deployment script output
staking_token_address = "0x97a54ad6993f1fbcae9fa75357a81eb85160120a" # Change to your deployed address

reward_rate = 10**16

# Encode the arguments
encoded_args = encode(['address', 'uint256'], [staking_token_address, reward_rate])

# Convert to hex string without '0x' prefix
hex_encoded_args = encoded_args.hex()

print(hex_encoded_args)

Then exectue the script:


python enode.py

The output will be something like (however the contract address should be the one you deployed):

00000000000000000000000097a54ad6993f1fbcae9fa75357a81eb85160120a000000000000000000000000000000000000000000000000002386f26fc10000

Use the encoded arguements above in the "Constructor Arguments ABI-encoded" field when deploying the StakingVault contract. Once the contracts are verified you will see a checkmark on the "Contract" tab icon.

There you have it, you just deployed an ERC-20 token along with a StakingVault contract. To challenge yourself, try to do the following next:


  1. Create Scripts to stake the "StakingToken" on the "StakingVault" contract
  2. Create a user interface (UI) to interact with the staking contract
  3. Test contract functionality on the public testnet (we only demonstrated it locally)

Final Thoughts​

In this guide, we've explored how to create a staking vault contract using ApeWorX and Vyper. We've covered the entire process from setting up the development environment to deploying and verifying our contracts on the Sepolia testnet.

We encourage you to experiment with the code, add new features, and most importantly, keep building! The blockchain ecosystem needs creative developers like you to drive innovation and create the next generation of web3 applications.

Stay up to date with the latest blockchain development tools and insights by following us on Twitter (@QuickNode) or joining our community on Discord. We're excited to see what you'll build next!

We ❀️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide