Documentation
For Solidity Developers
Developer Tools
Using Foundry With Polaris

Using Foundry with Polaris

Introduction

This guide shows how to deploy and interact with smart contracts using foundry on a local Polaris Ethereum EVM network.

Foundry toolchain (opens in a new tab) is a smart contract development toolchain written in Rust. It manages your dependencies, compiles your project, runs tests, deploys, and lets you interact with the chain from the command-line.

Recommended Knowledge

Requirements

Polaris Ethereum Liquid Staking Smart Contract Example

Create A New Project With forge

Let's create a new project called "polaris-lsd-example" with forge. forge init polaris-lsd-example

Add Configurations For Local Polaris Ethereum

💡

Ensure that you have a local instance of Polaris Ethereum Running. See Local Machine to run Polaris Ethereum locally.

You can add a profile to your foundry project by appending the following lines to the end of your foundry.toml file:

[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
 
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
 
[profile.polaris_local]
src = 'src'
out = 'out'
libs = ['lib']
evm_version = 'berlin'
eth_rpc_url = 'http://localhost:8545'

To test the configurations above, let's deploy the generated contract in the "Counter.sol" file to the Polaris Ethereum localnet:

FOUNDRY_PROFILE=polaris_local forge create Counter --private-key=xxxxxxxxxxx
ℹ️
  • Forge reads the profile's name from the environment variable "FOUNDRY_PROFILE". - You should replace xxxxxxxxxxx with your own private key.

If everything goes well, forge will print out the address of the deployed contract and the transaction hash for the deployment.

Install Dependencies

We will build the Liquid Staking contract based on Solmate (opens in a new tab) contracts.

To install solmate, run the following command:

forge install transmissions11/solmate

Add Remappings.txt

Foundry installs all dependencies as git submodules under the directory of "lib". And to make it easier to import the dependencies, let's create a remappings.txt file with the following contents:

ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/

Your file system should now look like this:

Create The Staking Module interface

We need a way of interacting with the staking module. This can be done through the use of an Interface. More information on Interfaces in Solidity can be found here (opens in a new tab)

Create a new file under the src directory called IStakingModule.sol and add the following code:

pragma solidity ^0.8.4;
 
/**
 * @dev Interface of the staking module's precompiled contract
 */
interface IStakingModule {
    ////////////////////////////////////////// EVENTS /////////////////////////////////////////////
 
    /**
     * @dev Emitted by the staking module when `amount` tokens are delegated to
     * `validator`
     */
    event Delegate(address indexed validator, uint256 amount);
 
    /**
     * @dev Emitted by the staking module when `amount` tokens are redelegated from
     * `sourceValidator` to `destinationValidator`
     */
    event Redelegate(
        address indexed sourceValidator,
        address indexed destinationValidator,
        uint256 amount
    );
 
    /**
     * @dev Emitted by the staking module when `amount` tokens are used to create `validator`
     */
    event CreateValidator(address indexed validator, uint256 amount);
 
    /**
     * @dev Emitted by the staking module when `amount` tokens are unbonded from `validator`
     */
    event Unbond(address indexed validator, uint256 amount);
 
    /**
     * @dev Emitted by the staking module when `amount` tokens are canceled from `delegator`'s
     * unbonding delegation with `validator`
     */
    event CancelUnbondingDelegation(
        address indexed validator,
        address indexed delegator,
        uint256 amount,
        int64 creationHeight
    );
 
    /////////////////////////////////////// READ METHODS //////////////////////////////////////////
 
    /**
     * @dev Returns a list of active validators.
     */
    function getActiveValidators() external view returns (address[] memory);
 
    /**
     * @dev Returns the `amount` of tokens currently delegated by `delegatorAddress` to
     * `validatorAddress`
     */
    function getDelegation(
        address delegatorAddress,
        address validatorAddress
    ) external view returns (uint256);
 
    /**
     * @dev Returns the `amount` of tokens currently delegated by `delegatorAddress` to
     * `validatorAddress` (at hex bech32 address)
     */
    function getDelegation(
        string calldata delegatorAddress,
        string calldata validatorAddress
    ) external view returns (uint256);
 
    /**
     * @dev Returns a time-ordered list of all UnbondingDelegationEntries between
     * `delegatorAddress` and `validatorAddress`
     */
    function getUnbondingDelegation(
        address delegatorAddress,
        address validatorAddress
    ) external view returns (UnbondingDelegationEntry[] memory);
 
    /**
     * @dev Returns a time-ordered list of all UnbondingDelegationEntries between
     * `delegatorAddress` and `validatorAddress` (at hex bech32 address)
     */
    function getUnbondingDelegation(
        string calldata delegatorAddress,
        string calldata validatorAddress
    ) external view returns (UnbondingDelegationEntry[] memory);
 
    /**
     * @dev Returns a list of `delegatorAddress`'s redelegating bonds from `srcValidator` to
     * `dstValidator`
     */
    function getRedelegations(
        address delegatorAddress,
        address srcValidator,
        address dstValidator
    ) external view returns (RedelegationEntry[] memory);
 
    /**
     * @dev Returns a list of `delegatorAddress`'s redelegating bonds from `srcValidator` to
     * `dstValidator` (at hex bech32 addresses)
     */
    function getRedelegations(
        string calldata delegatorAddress,
        string calldata srcValidator,
        string calldata dstValidator
    ) external view returns (RedelegationEntry[] memory);
 
    ////////////////////////////////////// WRITE METHODS //////////////////////////////////////////
 
    /**
     * @dev msg.sender delegates the `amount` of tokens to `validatorAddress`
     */
    function delegate(address validatorAddress, uint256 amount)
        external
        payable;
 
    /**
     * @dev msg.sender delegates the `amount` of tokens to `validatorAddress` (at hex bech32
     * address)
     */
    function delegate(string calldata validatorAddress, uint256 amount)
        external
        payable;
 
    /**
     * @dev msg.sender undelegates the `amount` of tokens from `validatorAddress`
     */
    function undelegate(address validatorAddress, uint256 amount)
        external
        payable;
 
    /**
     * @dev msg.sender undelegates the `amount` of tokens from `validatorAddress` (at hex bech32
     * address)
     */
    function undelegate(string calldata validatorAddress, uint256 amount)
        external
        payable;
 
    /**
     * @dev msg.sender redelegates the `amount` of tokens from `srcValidator` to `validtorDstAddr`
     */
    function beginRedelegate(
        address srcValidator,
        address dstValidator,
        uint256 amount
    ) external payable;
 
    /**
     * @dev msg.sender redelegates the `amount` of tokens from `srcValidator` to `validtorDstAddr`
     * (at hex bech32 addresses)
     */
    function beginRedelegate(
        string calldata srcValidator,
        string calldata dstValidator,
        uint256 amount
    ) external payable;
 
    /**
     * @dev Cancels msg.sender's unbonding delegation with `validatorAddress` and delegates the
     * `amount` of tokens back to `validatorAddress`
     *
     * Provide the `creationHeight` of the original unbonding delegation
     */
    function cancelUnbondingDelegation(
        address validatorAddress,
        uint256 amount,
        int64 creationHeight
    ) external payable;
 
    /**
     * @dev Cancels msg.sender's unbonding delegation with `validatorAddress` and delegates the
     * `amount` of tokens back to `validatorAddress` (at hex bech32 addresses)
     *
     * Provide the `creationHeight` of the original unbonding delegation
     */
    function cancelUnbondingDelegation(
        string calldata validatorAddress,
        uint256 amount,
        int64 creationHeight
    ) external payable;
 
    //////////////////////////////////////////// UTILS ////////////////////////////////////////////
 
    /**
     * @dev Represents one entry of an unbonding delegation
     *
     * Note: the field names of the native struct should match these field names (by camelCase)
     * Note: we are using the types in precompile/generated
     */
    struct UnbondingDelegationEntry {
        // creationHeight is the height which the unbonding took place
        int64 creationHeight;
        // completionTime is the unix time for unbonding completion, formatted as a string
        string completionTime;
        // initialBalance defines the tokens initially scheduled to receive at completion
        uint256 initialBalance;
        // balance defines the tokens to receive at completion
        uint256 balance;
        // unbondingingId incrementing id that uniquely identifies this entry
        uint64 unbondingId;
    }
 
    /**
     * @dev Represents a redelegation entry with relevant metadata
     *
     * Note: the field names of the native struct should match these field names (by camelCase)
     * Note: we are using the types in precompile/generated
     */
    struct RedelegationEntry {
        // creationHeight is the height which the redelegation took place
        int64 creationHeight;
        // completionTime is the unix time for redelegation completion, formatted as a string
        string completionTime;
        // initialBalance defines the initial balance when redelegation started
        uint256 initialBalance;
        // sharesDst is the amount of destination-validatorAddress shares created by redelegation
        uint256 sharesDst;
        // unbondingId is the incrementing id that uniquely identifies this entry
        uint64 unbondingId;
    }
}

Your file system should now look like this after adding IStakingModule.sol:

Create The Liquid Staking Contract

We are now going to create the Liquid Staking Contract. Create a new file under the src directory called LiquidStaking.sol and add the following code:

 
pragma solidity ^0.8.4;
 
import {IStakingModule} from "../staking.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
 
/**
 * @dev LiquidStaking is a contract that allows users to delegate their Base Denom to a validator
 * and receive a liquid staking token in return. The liquid staking token can be redeemed for Base
 * Denom at any time.
 * Note: This is an example of how to delegate Base Denom to a validator.
 * Doing it this way is unsafe since the user can delegate more straight through precomile.
 * And withdraw via the precompile.
 */
contract LiquidStaking is ERC20 {
    // State
    IStakingModule public staking;
    address public validatorAddress;
 
    // Errors
    error ZeroAddress();
    error ZeroAmount();
    error InvalidValue();
 
    /**
     * @dev Constructor that sets the staking precompile address and the validator address.
     * @param _name The name of the token.
     * @param _symbol The symbol of the token.
     * @param _stakingprecompile The address of the staking precompile contract.
     * @param _validatorAddress The address of the validator to delegate to.
     */
    constructor(
        string memory _name,
        string memory _symbol,
        address _stakingprecompile,
        address _validatorAddress
    ) ERC20(_name, _symbol, 18) {
        if (_stakingprecompile == address(0)) revert ZeroAddress();
        if (_validatorAddress == address(0)) revert ZeroAddress();
        staking = IStakingModule(_stakingprecompile);
        validatorAddress = _validatorAddress;
    }
 
    /**
     * @dev Returns the total amount of assets delegated to the validator.
     * @return amount total amount of assets delegated to the validator.
     */
    function totalDelegated() public view returns (uint256 amount) {
        return staking.getDelegation(address(this), validatorAddress);
    }
 
    /**
     * @dev Delegates Base Denom to the validator.
     * @param amount amount of Base Denom to delegate.
     */
    function delegate(uint256 amount) public {
        if (amount == 0) revert ZeroAmount();
 
        // Delegate the amount to the validator.
        staking.delegate(validatorAddress, amount);
        _mint(msg.sender, amount);
    }
 
    /**
     * @dev Withdraws Base Denom from the validator.
     * @param amount amount of Base Denom to withdraw.
     */
    function withdraw(uint256 amount) public {
        if (amount == 0) revert ZeroAmount();
        _burn(msg.sender, amount);
        payable(msg.sender).transfer(amount);
    }
}

Your file system should now look like this after adding LiquidStaking.sol:

Functionality

LiquidStaking.sol is a Solidity smart contract that implements a basic liquid staking mechanism, allowing users to delegate tokens to a validator and receive staked tokens in return. The key features of the contract are:

  • It inherits from the ERC20 token contract, allowing it to be used as a standard fungible token with a name, symbol, and decimal places.
  • It takes four arguments in its constructor: the name and symbol of the LSD, the address of Staking Module Precompile Contract Address, and the address of the validator that the contract will delegate to.
  • The delegate(uint256) function allows users to delegate an amount of the token to the validator, by calling the staking.delegate() function with the same amount of the token transferred as Ether.
  • The withdraw(uint256) function allows users to withdraw their delegated tokens from the validator, by burning their staked tokens and transferring the same amount of Ether back to the user.
  • The totalDelegated() function returns the total amount of assets currently delegated to the validator from the contract address.

Overall, this contract allows users to stake their tokens through a validator, receiving a tokenized version of their stake that can be traded or transferred like any other ERC20 token. Note: this is an example and not production code.

Deploy The Liquid Staking Contract

To deploy your Liquid Staking contract you can run the following script:

💡

The default staking precompile contract address is 0xd9A998CaC66092748FfEc7cFBD155Aae1737C2fF. More information about precompiles can be found here

FOUNDRY_PROFILE=polaris_local forge create src/LiquidStaking.sol:LiquidStaking --private-key=xxxxxxxxxxx --constructor-args  “<name>“ “<symbol>” “<staking_precompiled_contract_address>” “<validator_address>

or you can use a forge script to handle this.

pragma solidity ^0.8.4;
 
import "forge-std/Script.sol";
import "./LiquidStaking.sol";
import "../staking.sol";
 
contract Deploy is Script {
    LiquidStaking public staking;
    address immutable precompile =
        address(0xd9A998CaC66092748FfEc7cFBD155Aae1737C2fF);
    address public validator = address(0x12); // @dev : Change this to the validator running.
 
    function run() public {
        vm.startBroadcast();
        staking = new LiquidStaking("name", "SYMB", precompile, validator);
        vm.stopBroadcast();
    }
}
FOUNDRY_PROFILE=polaris_local forge script src/examples/Deploy.s.sol:Deploy --broadcast  --private-key=xxxxxxx --rpc-url=xxxx --gas-limit 10000000

If everything goes well, forge will print out the address of the deployed contract and the transaction hash for the deployment.

Test The Liquid Staking Contract

We can now test our Liquid Staking contract by delegating and receiving a tokenized version of our stake. To do so, run the following script:

FOUNDRY_PROFILE=polaris_local cast send --private-key=xxxxxxxxxxx <deployed_contract_address> ‘delegate()1

🎉 Congratulations! 🎉

Congratulations! You have successfully created and deployed a Liquid Staking contract on Polaris Ethereum via Foundry!