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
- Basic understanding of Solidity (opens in a new tab) and Cosmos (opens in a new tab).
- Basic understanding of the Polaris Ethereum's architecture
- Basic understanding of Liquid Staking (opens in a new tab)
Requirements
- You have installed
Foundry (opens in a new tab) and run
foundryup
. This installation includes theforge
andcast
binaries used in this walk-through. - You have a running instance of Polaris Ethereum EVM. See Local Machine to run Polaris Ethereum locally.
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 thestaking.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!