Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
"solidity.linter": "solhint",
"solidity.packageDefaultDependenciesContractsDirectory": "src",
"solidity.packageDefaultDependenciesDirectory": "lib"
}
}
125 changes: 125 additions & 0 deletions docs/native-lst-third-party-integrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Integrating Tenderize **Sei** Native-Asset Multi-Validator LST (tSEI)

> **Audience:** Oracle providers, lending protocol developers, ...

Tenderize's multi-validator Liquid Staking Token for **SEI (`tSEI`)** continuously accrues staking rewards and is fully backed by on-chain, non-custodial validator positions managed by the Tenderize Protocol on the Sei Network.
This guide explains how developers can integrate **tSEI** into their protocols:

1. **Redemption-Rate Oracles** – track the exchange-rate between an LST and its underlying native asset (e.g. `tSEI ⇆ SEI`).
2. **Lending Protocols** – consume redemption-rate oracles, account for staking yield, and execute liquidations via Tenderize-specific mechanisms or third-party liquidity pools (`unstake`, `withdraw` or `flashUnstake`).

For more information about Tenderize's Sei LSTs please refer to the [Sei adapter documentation](../src/tenderize-v3/Sei/README.md)

---

## 1. Redemption-Rate Oracles

### 1.1 What is the *redemption rate*?

The redemption rate is the amount of underlying native asset redeemable per 1 unit of LST:

$$R(t) = \frac{\text{Underlying Balance at } t}{\text{tSEI Supply at } t}$$

Because staking rewards are auto-compounded inside Tenderize, `R(t)` increases monotonically.

### 1.2 Why use a redemption-rate oracle?

• Removes reliance on secondary-market prices which can de-peg during stress.
• Accurately reflects accrued rewards.
• Enables lending protocols to value LST collateral precisely and trigger liquidations before the position becomes under-collateralized.

### 1.3 Oracle reference implementation

Oracle providers can derive `R(t)` directly from the **MultiValidatorLST** contract:

```solidity
interface IMultiValidatorLST {
function exchangeRate() external view returns (uint256);
}

contract SeiRedemptionOracle {
IMultiValidatorLST public immutable tSEI;

constructor(address _tSEI) {
tSEI = IMultiValidatorLST(_tSEI);
}

// Returns the redemption rate scaled to 1e18 (uSEI per tSEI)
function latestAnswer() external view returns (uint256) {
return tSEI.exchangeRate();
}
}
```

`exchangeRate()` is a `1e18`-scaled fixed-point number representing **underlying SEI per 1 tSEI**.

### 1.4 Recommended parameters

• **Heartbeat:** 15 minutes – LSTs update slowly; more frequent pushes offer diminishing returns.
• **Deviation threshold:** 0.01% – Redemption rate increments are small but monotonic; flag larger jumps.
• **Decimals:** 18 – matches underlying native token.

### 1.5 Sei-specific considerations

• **Decimals:** Sei's native token uses **6 decimals** (`1 SEI = 1,000,000 uSEI`). `tSEI` keeps the standard **18 decimals**. The adapter internally scales values by `1e12` when interacting with the precompile.
• **Validator IDs:** Tenderize represents validators as `bytes32`. The Sei adapter converts these to **bech32** validator addresses (`seivaloper…`) under the hood; integrators never need to supply bech32 manually.
• **Conversion helpers:** For convenience, the `SeiAdapter` now exposes `validatorStringToBytes32()` and `validatorBytes32ToString()` so external tools can convert between the `seivaloper…` address format and the internal `bytes32` representation if desired.
• **Unbonding period:** 21 days.

---

## 2. Lending Protocol Integration

### 2.1 Valuing collateral

Use the redemption-rate oracle to convert a user's LST balance to underlying native units, then price via your existing native-asset oracle.

\[
\text{Collateral Value} = R(t) \times P_{\text{native}}
\]

Where `P_native` is the USD price of the native asset.

### 2.2 Accounting for staking yield

Because `R(t)` grows, collateral value increases automatically.
No additional accounting is required from the protocol side.

### 2.3 Liquidation workflow

When a borrower falls below the liquidation threshold, governors may choose between four execution paths:

1. **Unstake & Wait (gas-efficient)**
• Call `unstake(shares, minAmount)` on the `tSEI` contract. The tx returns an `unstakeID`.
• After the 21-day Sei unbonding period, call `withdraw(unstakeID)` to receive SEI.

2. **Immediate withdraw (if mature)**
If 21 days have already passed, go straight to `withdraw(unstakeID)` and seize the SEI.

3. **Instant exit via Flash Unstake (`FlashUnstakeNative`)**
Use the helper at ``FlashUnstakeNative.flashUnstake(...)` to unwrap the validator positions and swap into SEI within a single transaction. This uses TenderSwap underneath, the amount received depends on the available liquidity:

```solidity
FlashUnstake(0xFlashUnstake).flashUnstake(
0xtSEI, // tSEI token address
0xTenderSwap, // TenderSwap pool that holds SEI liquidity
tseiAmount, // amount of tSEI to liquidate
minSeiOut // slippage guard
);
```

4. **Sell on DEX**
If sufficient secondary-market liquidity exists, liquidators can simply swap `tSEI → SEI` on a DEX.

Liquidators can choose between these paths based on gas costs, urgency, and liquidity conditions.

---

## 3. Contract Addresses (Mainnet)

| Name | Address | Description |
|-------|--------|------------------------|
| tSEI | `0x0027305D78Accd068d886ac4217B67922E9F490f` | Multi-validator LST token managed by the Tenderize protocol and governance |
| FlashUnstake | `0x0724788Cdab1f059cA9d7FCD9AA9513BB9A984f8` | Wrapper that unwraps `tSEI` into single-validator LST parts and sells them on TenderSwap, used to instantly unstake `tSEI` without unstaking period |
| TenderSwap (Sei) | `0x5c57F4E063a2A1302D78ac9ec2C902ec621200d3` | Instantly unstake staked SEI for a small fee |
| Single-validator LST factory | `0xb0E174D9235f133333c71bB47120e4Cb26442386` | Create liquid staking vaults tied to a specific Sei validator, extending the delegation experience with liquid staking |
30 changes: 25 additions & 5 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ bytecode_hash = "none"
fuzz = { runs = 1_000 }
gas_reports = ["*"]
libs = ["lib"]
evm_version = "shanghai"
evm_version = "cancun"
# optimizer = true (default)
optimizer_runs = 200
fs_permissions = [{ access = "read-write", path = "./" }]
Expand All @@ -29,8 +29,28 @@ fail_on_revert = false
runs = 256
depth = 100


[etherscan]
arbitrum = { key = "${API_KEY_ARBISCAN}" }
# avalanche = { key = "${API_KEY_SNOWTRACE}" }
# bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" }
# gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" }
# goerli = { key = "${API_KEY_ETHERSCAN}" }
mainnet = { key = "${API_KEY_ETHERSCAN}" }
# optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" }
# polygon = { key = "${API_KEY_POLYGONSCAN}" }
# sepolia = { key = "${API_KEY_ETHERSCAN}" }
sei_testnet = { key = "", url = "https://seitrace.com/atlantic-2/api" }

[rpc_endpoints]
# Uncomment to enable the RPC server
arbitrum_goerli = "${ARBITRUM_GOERLI_RPC}"
arbitrum = "${ARBITRUM_RPC}"
mainnet = "${MAINNET_RPC}"
arbitrum = "https://arb-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}"
# avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}"
# bnb_smart_chain = "https://bsc-dataseed.binance.org"
# gnosis_chain = "https://rpc.gnosischain.com"
# goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}"
# localhost = "http://localhost:8545"
mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}"
# optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}"
# polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}"
# sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}"
sei_testnet = "https://sei-testnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}"
3 changes: 3 additions & 0 deletions script/Adapter_Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Registry } from "core/registry/Registry.sol";
import { LivepeerAdapter, LPT, VERSION as LPT_VERSION } from "core/adapters/LivepeerAdapter.sol";
import { GraphAdapter, GRT, VERSION as GRT_VERSION } from "core/adapters/GraphAdapter.sol";
import { PolygonAdapter, POL, VERSION as POL_VERSION } from "core/adapters/PolygonAdapter.sol";
import { SeiAdapter, SEI, VERSION as SEI_VERSION } from "core/tenderize-v3/Sei/SeiAdapter.sol";

contract Adapter_Deploy is Script {
uint256 VERSION;
Expand All @@ -46,6 +47,8 @@ contract Adapter_Deploy is Script {
adapter = address(new GraphAdapter{ salt: bytes32(GRT_VERSION) }());
} else if (asset == address(POL)) {
adapter = address(new PolygonAdapter{ salt: bytes32(POL_VERSION) }());
} else if (asset == address(SEI)) {
adapter = address(new SeiAdapter());
} else {
revert("Adapter not supported");
}
Expand Down
28 changes: 28 additions & 0 deletions script/Create2Factory.deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
//
// Simple script to deploy the `Create2Factory` on any EVM-compatible network (e.g. Sei testnet)
// and print the deployed address. Run with:
// forge script script/Create2Factory.deploy.s.sol --broadcast --rpc-url <SEI_RPC> --private-key $PRIVATE_KEY
//
// Contracts are deployed normally (not via create2) because the factory itself is responsible for
// subsequent CREATE2 deployments.
//
// solhint-disable no-console

pragma solidity >=0.8.19;

import { Script, console2 } from "forge-std/Script.sol";

import { Create2Deployer } from "core/utils/Create2Deployer.sol";

contract Create2FactoryDeploy is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

Create2Deployer factory = new Create2Deployer();
console2.log("Create2Deployer deployed at:", address(factory));

vm.stopBroadcast();
}
}
45 changes: 38 additions & 7 deletions script/Run.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,70 @@

// solhint-disable no-console

pragma solidity 0.8.20;
pragma solidity 0.8.25;

import { Script, console2 } from "forge-std/Script.sol";

import { MultiValidatorLST } from "core/multi-validator/MultiValidatorLST.sol";
import { MultiValidatorFactory } from "core/multi-validator/Factory.sol";
import { FlashUnstake, TenderSwap } from "core/multi-validator/FlashUnstake.sol";
<<<<<<< HEAD
import { Tenderizer } from "core/tenderizer/Tenderizer.sol";
=======
import { Tenderizer } from "core/tenderize-v3/Tenderizer.sol";
>>>>>>> 890b534 (Sei testnet deployment)
import { LPT } from "core/adapters/LivepeerAdapter.sol";

import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol";
import { FlashUnstake } from "core/multi-validator/FlashUnstake.sol";

import { SeiAdapter } from "core/tenderize-v3/Sei/SeiAdapter.sol";
import { ISeiStaking, Delegation } from "core/tenderize-v3/Sei/Sei.sol";

address constant TENDERIZER_1 = 0x4b7339E599a599DBd7829a8ECA0d233ED4F7eA09;
address constant TENDERIZER_2 = 0xFB32bF22B4F004a088c1E7d69e29492f5D7CD7E1;
address constant TENDERIZER_3 = 0x6DFd5Cee0Ed2ec24Fdc814Ad857902DE01c065d6;
address constant LIVEPEER_MINTER = 0xc20DE37170B45774e6CD3d2304017fc962f27252;

interface ADDR {
function getSeiAddr(address addr) external view returns (string memory response);
function getEvmAddr(string memory addr) external view returns (address response);
}

contract MultiValidatorLST_Deploy is Script {
bytes32 private constant salt = bytes32(uint256(1));

MultiValidatorFactory factory;
// MultiValidatorLST lst;

function run() public {
uint256 privKey = vm.envUint("PRIVATE_KEY");
address guy = 0xF77fc3ae854164EAd1eeb39458d830Cd464270eD;
address lst = 0xeab62Fb116f2e1f766A8a64094389553a00C2F68;
vm.startBroadcast(guy);
address constant ADDR_PRECOMPILE = 0x0000000000000000000000000000000000001004;
address constant STAKING_PRECOMPILE = 0x0000000000000000000000000000000000001005;

Tenderizer(payable(lst)).withdraw(guy, 64);
function run() public payable {
uint256 privKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privKey);
// console2.logString(
// SeiAdapter(0x59726AcA54DB5bA44888992A88e71af1E2D2f09C).validatorBytes32ToString(
// 0x2af815558b165be177531446f693fb7e7f3563e1000000000000000000000000
// )
// );

Delegation memory del = ISeiStaking(STAKING_PRECOMPILE).delegation(
0x28D5bC07301472829bab14aC26CF74676e9FB1d3, "seivaloper19tup24vtzed7za6nz3r0dylm0eln2clpvhtawu"
);
console2.log("del", del.balance.amount);
console2.log("del", del.balance.denom);
console2.log("del", del.delegation.delegator_address);
console2.log("del", del.delegation.shares);
console2.log("del", del.delegation.decimals);
console2.log("del", del.delegation.validator_address);
// SeiAdapter adapter = SeiAdapter(0xc7324079ACD020c2585DD00bc734d1a799D675fd);
// (ok, ret) = adapter.debugRawDelegation(0x2af815558b165be177531446f693fb7e7f3563e1000000000000000000000000);
// console2.log("ok", ok);
// console2.logBytes(ret);
// address payable lst = payable(0x28D5bC07301472829bab14aC26CF74676e9FB1d3);
// Tenderizer(lst).deposit{ value: 1 ether }(msg.sender);
vm.stopBroadcast();
}
}
83 changes: 83 additions & 0 deletions script/Tenderize_Native_Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT
//
// _____ _ _
// |_ _| | | (_)
// | | ___ _ __ __| | ___ _ __ _ _______
// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \
// | | __/ | | | (_| | __/ | | |/ / __/
// \_/\___|_| |_|\__,_|\___|_| |_/___\___|
//
// Copyright (c) Tenderize Labs Ltd

// solhint-disable no-console

pragma solidity >=0.8.19;

import { Script, console2 } from "forge-std/Script.sol";
import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol";

import { Tenderizer } from "core/tenderize-v3/Tenderizer.sol";
import { TenderizerFactory } from "core/tenderize-v3/Factory.sol";
import { Registry } from "core/tenderize-v3/registry/Registry.sol";
import { FACTORY_ROLE } from "core/registry/Roles.sol";
import { Renderer } from "core/unlocks/Renderer.sol";
import { Unlocks } from "core/unlocks/Unlocks.sol";

uint256 constant VERSION = 1;

contract Tenderize_Native_Deploy is Script {
// Contracts are deployed deterministically using CREATE2 via forge`s deterministic-deployment-proxy.
bytes32 private constant salt = bytes32(VERSION);

function run() public {
string memory json_output;

// Start broadcasting with private key from `.env` file
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

// 1. Deploy Registry (without initialization)
Registry registryImpl = new Registry();
address registryProxy = address(new ERC1967Proxy(address(registryImpl), ""));
vm.serializeAddress(json_output, "registry_implementation", address(registryImpl));
vm.serializeAddress(json_output, "registry_proxy", registryProxy);
console2.log("Registry Implementation: ", address(registryImpl));
console2.log("Registry Proxy: ", registryProxy);

// 2. Deploy Unlocks
// - Deploy Renderer Implementation
Renderer rendererImpl = new Renderer();
vm.serializeAddress(json_output, "renderer_implementation", address(rendererImpl));
// - Deploy Renderer UUPS Proxy
ERC1967Proxy rendererProxy = new ERC1967Proxy(address(rendererImpl), abi.encodeCall(rendererImpl.initialize, ()));
vm.serializeAddress(json_output, "renderer_proxy", address(rendererProxy));
// - Deploy Unlocks
Unlocks unlocks = new Unlocks(registryProxy, address(rendererProxy));
vm.serializeAddress(json_output, "unlocks", address(unlocks));
console2.log("Renderer Implementation: ", address(rendererImpl));
console2.log("Renderer Proxy: ", address(rendererProxy));
console2.log("Unlocks: ", address(unlocks));

// 3. Deploy Tenderizer Implementation (native asset)
address asset = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; // Native ETH
Tenderizer tenderizerImpl = new Tenderizer(asset, registryProxy, address(unlocks));
vm.serializeAddress(json_output, "tenderizer_implementation", address(tenderizerImpl));
console2.log("Tenderizer Implementation: ", address(tenderizerImpl));

// 4. Initialize Registry
Registry(registryProxy).initialize(address(tenderizerImpl), address(unlocks));

// 5. Deploy TenderizerFactory (UpgradeableBeacon) and register it
TenderizerFactory factory = new TenderizerFactory(registryProxy, address(tenderizerImpl));
vm.serializeAddress(json_output, "factory", address(factory));
console2.log("Factory (Beacon): ", address(factory));

// - Grant FACTORY_ROLE to Factory
Registry(registryProxy).grantRole(FACTORY_ROLE, address(factory));

vm.stopBroadcast();

// Write json_output to file if desired
// vm.writeJson(json_output, "deployments_native.json");
}
}
Loading
Loading