Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SEI Integration #76

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
feat: SEI adapter
kyriediculous committed Apr 2, 2024
commit fb40eab2c9b473215467dbb30a619aeca9228371
4 changes: 3 additions & 1 deletion src/adapters/Adapter.sol
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ import { IERC165 } from "core/interfaces/IERC165.sol";

pragma solidity >=0.8.19;

error WithdrawPending();

interface Adapter is IERC165 {
function previewDeposit(address validator, uint256 assets) external view returns (uint256);

@@ -24,7 +26,7 @@ interface Adapter is IERC165 {

function currentTime() external view returns (uint256);

function stake(address validator, uint256 amount) external returns (uint256 staked);
function stake(address validator, uint256 amount) external payable returns (uint256 staked);

function unstake(address validator, uint256 amount) external returns (uint256 unlockID);

6 changes: 2 additions & 4 deletions src/adapters/GraphAdapter.sol
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ uint256 constant VERSION = 1;

import { ERC20 } from "solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { Adapter } from "core/adapters/Adapter.sol";
import { Adapter, WithdrawPending } from "core/adapters/Adapter.sol";
import { IGraphStaking, IGraphEpochManager } from "core/adapters/interfaces/IGraph.sol";
import { IERC165 } from "core/interfaces/IERC165.sol";

@@ -29,8 +29,6 @@ contract GraphAdapter is Adapter {

uint256 private constant STORAGE = uint256(keccak256("xyz.tenderize.graph.adapter.storage.location")) - 1;

error WithdrawPending();

struct Unlock {
uint256 shares;
uint256 epoch;
@@ -109,7 +107,7 @@ contract GraphAdapter is Adapter {
return GRAPH_STAKING.hasStake(validator);
}

function stake(address validator, uint256 amount) external override returns (uint256) {
function stake(address validator, uint256 amount) external payable override returns (uint256) {
GRT.safeApprove(address(GRAPH_STAKING), amount);
uint256 delShares = GRAPH_STAKING.delegate(validator, amount);
IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);
2 changes: 1 addition & 1 deletion src/adapters/LivepeerAdapter.sol
Original file line number Diff line number Diff line change
@@ -89,7 +89,7 @@ contract LivepeerAdapter is Adapter {
return block.number;
}

function stake(address validator, uint256 amount) public returns (uint256) {
function stake(address validator, uint256 amount) public payable returns (uint256) {
LPT.safeApprove(address(LIVEPEER_BONDING), amount);
LIVEPEER_BONDING.bond(amount, validator);
return amount;
2 changes: 1 addition & 1 deletion src/adapters/PolygonAdapter.sol
Original file line number Diff line number Diff line change
@@ -124,7 +124,7 @@ contract PolygonAdapter is Adapter {
return POLYGON_STAKEMANAGER.epoch();
}

function stake(address validator, uint256 amount) external override returns (uint256) {
function stake(address validator, uint256 amount) external payable override returns (uint256) {
// approve tokens
POL.safeApprove(address(POLYGON_STAKEMANAGER), amount);

107 changes: 107 additions & 0 deletions src/adapters/SeiAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
//
// _____ _ _
// |_ _| | | (_)
// | | ___ _ __ __| | ___ _ __ _ _______
// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \
// | | __/ | | | (_| | __/ | | |/ / __/
// \_/\___|_| |_|\__,_|\___|_| |_/___\___|
//
// Copyright (c) Tenderize Labs Ltd

pragma solidity >=0.8.19;

uint256 constant VERSION = 1;

import { ERC20 } from "solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { Adapter, WithdrawPending as WithdrawPendingError } from "core/adapters/Adapter.sol";
import { ISei, StakingPool, UnbondingDelegation, BondStatus, UNSTAKE_TIME } from "core/adapters/interfaces/ISei.sol";
import { IERC165 } from "core/interfaces/IERC165.sol";
import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol";

address constant SEI_STAKING_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001005;
ISei constant SEI_STAKING_CONTRACT = ISei(SEI_STAKING_PRECOMPILE_ADDRESS);

contract SeiAdapter is Adapter {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;

uint256 private constant STORAGE = uint256(keccak256("xyz.tenderize.sei.adapter.storage.location")) - 1;

struct Storage {
address validator;
}

function _loadStorage() internal pure returns (Storage storage $) {
uint256 slot = STORAGE;

// solhint-disable-next-line no-inline-assembly
assembly {
$.slot := slot
}
}

function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId;
}

function previewDeposit(address validator, uint256 assets) external view override returns (uint256) {
return _previewDeposit(validator, assets);
}

function previewWithdraw(uint256 unlockID) external view override returns (uint256) {
UnbondingDelegation memory unbond = SEI_STAKING_CONTRACT.getUnbondingDelegation(_loadStorage().validator, unlockID);
return unbond.amount;
}

function unlockMaturity(uint256 unlockID) external view override returns (uint256) {
UnbondingDelegation memory unbond = SEI_STAKING_CONTRACT.getUnbondingDelegation(_loadStorage().validator, unlockID);
return unbond.completionTime;
}

function unlockTime() external view override returns (uint256) {
return UNSTAKE_TIME;
}

function currentTime() external view override returns (uint256) {
return block.timestamp;
}

function isValidator(address validator) public view override returns (bool) {
return SEI_STAKING_CONTRACT.getStakingPool(validator).status == BondStatus.Bonded;
}

function stake(address validator, uint256 amount) external payable override returns (uint256 out) {
out = _previewDeposit(validator, amount);
SEI_STAKING_CONTRACT.delegate(validator, amount);
}

function unstake(address validator, uint256 amount) external override returns (uint256 unlockID) {
unlockID = SEI_STAKING_CONTRACT.undelegate(validator, amount);
}

function withdraw(address validator, uint256 unlockID) external override returns (uint256 amount) {
UnbondingDelegation memory unbond = SEI_STAKING_CONTRACT.getUnbondingDelegation(validator, unlockID);
// TODO: check unbonding time denomination
if (unbond.completionTime > block.timestamp) {
revert WithdrawPendingError();
}
amount = unbond.amount;
}

function rebase(address validator, uint256 /*currentStake*/ ) external override returns (uint256 newStake) {
Storage storage $ = _loadStorage();
if ($.validator == address(0)) $.validator = validator;

uint256 shares = SEI_STAKING_CONTRACT.getDelegation(address(this), validator);
StakingPool memory stakingPool = SEI_STAKING_CONTRACT.getStakingPool(validator);
newStake = shares.mulDivDown(stakingPool.totalTokens, stakingPool.totalShares);
}

function _previewDeposit(address validator, uint256 assets) internal view returns (uint256 out) {
StakingPool memory stakingPool = SEI_STAKING_CONTRACT.getStakingPool(validator);
uint256 shares = assets.mulDivDown(stakingPool.totalShares, stakingPool.totalTokens);
return shares.mulDivDown(stakingPool.totalTokens + assets, stakingPool.totalShares + shares);
}
}
49 changes: 49 additions & 0 deletions src/adapters/interfaces/ISei.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
//
// _____ _ _
// |_ _| | | (_)
// | | ___ _ __ __| | ___ _ __ _ _______
// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \
// | | __/ | | | (_| | __/ | | |/ / __/
// \_/\___|_| |_|\__,_|\___|_| |_/___\___|
//
// Copyright (c) Tenderize Labs Ltd

pragma solidity >=0.8.19;

uint256 constant UNSTAKE_TIME = 21 days;

enum BondStatus {
Unbonded,
Unbonding,
Bonded
}

struct UnbondingDelegation {
uint256 initialAmount;
uint256 amount;
uint256 creationHeight;
uint256 completionTime;
}

struct StakingPool {
uint256 totalShares;
uint256 totalTokens;
BondStatus status;
bool jailed;
}

interface ISei {
// Transactions
function delegate(address validator, uint256 amount) external returns (bool success);

function redelegate(address src, address dst, uint256 amount) external returns (bool success);

function undelegate(address validator, uint256 amount) external returns (uint256 unbondingID);

function getDelegation(address delegator, address validator) external view returns (uint256 shares);

function getStakingPool(address validator) external view returns (StakingPool memory);

function getUnbondingDelegation(address validator, uint256 unbondingID) external view returns (UnbondingDelegation memory);
}
226 changes: 226 additions & 0 deletions src/tenderizer/TenderizerETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// SPDX-License-Identifier: MIT
//
// _____ _ _
// |_ _| | | (_)
// | | ___ _ __ __| | ___ _ __ _ _______
// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \
// | | __/ | | | (_| | __/ | | |/ / __/
// \_/\___|_| |_|\__,_|\___|_| |_/___\___|
//
// Copyright (c) Tenderize Labs Ltd

pragma solidity >=0.8.19;

import { ERC20 } from "solmate/tokens/ERC20.sol";
import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";

import { Adapter, AdapterDelegateCall } from "core/adapters/Adapter.sol";
import { Registry } from "core/registry/Registry.sol";
import { TenderizerImmutableArgs, TenderizerEvents } from "core/tenderizer/TenderizerBase.sol";
import { TToken } from "core/tendertoken/TToken.sol";
import { Multicall } from "core/utils/Multicall.sol";
import { SelfPermit } from "core/utils/SelfPermit.sol";
import { _staticcall } from "core/utils/StaticCall.sol";
import { addressToString } from "core/utils/Utils.sol";

/**
* @title Tenderizer
* @author Tenderize Labs Ltd
* @notice Liquid staking vault for native liquid staking
* @dev Uses full type safety and unstructured storage
*/

contract Tenderizer is TenderizerImmutableArgs, TenderizerEvents, TToken, Multicall, SelfPermit {
error InsufficientAssets();

using AdapterDelegateCall for Adapter;
using FixedPointMathLib for uint256;
using SafeTransferLib for ERC20;

uint256 private constant MAX_FEE = 0.005e6; // 0.5%
uint256 private constant FEE_BASE = 1e6;

// solhint-disable-next-line no-empty-blocks
constructor(address _registry, address _unlocks) TenderizerImmutableArgs(_registry, _unlocks) { }
receive() external payable { }
fallback() external payable { }

// @inheritdoc TToken
function name() external view override returns (string memory) {
return string.concat("tender ", _baseSymbol());
}

// @inheritdoc TToken
function symbol() external view override returns (string memory) {
return string.concat("t", _baseSymbol());
}

// @inheritdoc TToken
function transfer(address to, uint256 amount) public override returns (bool) {
_rebase();
return TToken.transfer(to, amount);
}

// @inheritdoc TToken
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
_rebase();
return TToken.transferFrom(from, to, amount);
}

/**
* @notice Deposit assets to mint tTokens
* @param receiver address to mint tTokens to
* @param assets amount of assets to deposit
*/
function deposit(address receiver, uint256 assets) external payable returns (uint256) {
_rebase();

// check if value is sent with message
if (msg.value < assets) revert();

// stake assets
uint256 staked = _stake(validator(), assets);

// mint tokens to receiver
uint256 shares;
if ((shares = _mint(receiver, staked)) == 0) revert InsufficientAssets();

uint256 tTokenOut = convertToAssets(shares);
emit Deposit(msg.sender, receiver, assets, tTokenOut);

return tTokenOut;
}

/**
* @notice Unlock tTokens to withdraw assets at maturity
* @param assets amount of assets to unlock
* @return unlockID of the unlock
*/
function unlock(uint256 assets) external returns (uint256 unlockID) {
_rebase();

// burn tTokens before creating an `unlock`
_burn(msg.sender, assets);

// unlock assets and get unlockID
unlockID = _unstake(validator(), assets);

// create unlock of unlockID
_unlocks().createUnlock(msg.sender, unlockID);

// emit Unlock event
emit Unlock(msg.sender, assets, unlockID);
}

/**
* @notice Redeem an unlock to withdraw assets after maturity
* @param receiver address to withdraw assets to
* @param unlockID ID of the unlock to redeem
* @return amount of assets withdrawn
*/
function withdraw(address payable receiver, uint256 unlockID) external returns (uint256 amount) {
// Redeem unlock if mature
_unlocks().useUnlock(msg.sender, unlockID);

// withdraw assets to send to `receiver`
amount = _withdraw(validator(), unlockID);

// transfer assets to `receiver`
receiver.transfer(amount);

// emit Withdraw event
emit Withdraw(receiver, amount, unlockID);
}

/**
* @notice Rebase tToken supply
* @dev Rebase can be called by anyone, is also forced to be called before any action or transfer
*/
function rebase() external {
_rebase();
}

function _rebase() internal {
uint256 currentStake = totalSupply();
uint256 newStake = _rebase(validator(), currentStake);

if (newStake > currentStake) {
unchecked {
uint256 rewards = newStake - currentStake;
uint256 fees = _calculateFees(rewards);
_setTotalSupply(newStake - fees);
// mint fees
if (fees > 0) {
_mint(_registry().treasury(), fees);
}
}
} else {
_setTotalSupply(newStake);
}

// emit rebase event
emit Rebase(currentStake, newStake);
}

function _calculateFees(uint256 rewards) internal view returns (uint256 fees) {
uint256 fee = _registry().fee(asset());
fee = fee > MAX_FEE ? MAX_FEE : fee;
fees = rewards * fee / FEE_BASE;
}

function _baseSymbol() internal view returns (string memory) {
return string.concat(ERC20(asset()).symbol(), "-", addressToString(validator()));
}

function previewDeposit(uint256 assets) external view returns (uint256) {
uint256 out = abi.decode(_staticcall(address(this), abi.encodeCall(this._previewDeposit, (assets))), (uint256));
Storage storage $ = _loadStorage();
uint256 _totalShares = $._totalShares; // Saves an extra SLOAD if slot is non-zero
uint256 shares = convertToShares(out);
return _totalShares == 0 ? out : shares * $._totalSupply / _totalShares;
}

function previewWithdraw(uint256 unlockID) external view returns (uint256) {
return abi.decode(_staticcall(address(this), abi.encodeCall(this._previewWithdraw, (unlockID))), (uint256));
}

function unlockMaturity(uint256 unlockID) external view returns (uint256) {
return abi.decode(_staticcall(address(this), abi.encodeCall(this._unlockMaturity, (unlockID))), (uint256));
}

// ===============================================================================================================
// NOTE: These functions are marked `public` but considered `internal` (hence the `_` prefix).
// This is because the compiler doesn't know whether there is a state change because of `delegatecall``
// So for the external API (e.g. used by Unlocks.sol) we wrap these functions in `external` functions
// using a `staticcall` to `this`.
// This is a hacky workaround while better solidity features are being developed.
function _previewDeposit(uint256 assets) public returns (uint256) {
return abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().previewDeposit, (validator(), assets))), (uint256));
}

function _previewWithdraw(uint256 unlockID) public returns (uint256) {
return abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().previewWithdraw, (unlockID))), (uint256));
}

function _unlockMaturity(uint256 unlockID) public returns (uint256) {
return abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().unlockMaturity, (unlockID))), (uint256));
}
// ===============================================================================================================

function _rebase(address validator, uint256 currentStake) internal returns (uint256 newStake) {
newStake = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().rebase, (validator, currentStake))), (uint256));
}

function _stake(address validator, uint256 amount) internal returns (uint256 staked) {
staked = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().stake, (validator, amount))), (uint256));
}

function _unstake(address validator, uint256 amount) internal returns (uint256 unlockID) {
unlockID = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().unstake, (validator, amount))), (uint256));
}

function _withdraw(address validator, uint256 unlockID) internal returns (uint256 withdrawAmount) {
withdrawAmount = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().withdraw, (validator, unlockID))), (uint256));
}
}