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

Nv/polygon integration #62

Merged
merged 2 commits into from
Sep 14, 2023
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
154 changes: 154 additions & 0 deletions src/adapters/PolygonAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: MIT
//
// _____ _ _
// |_ _| | | (_)
// | | ___ _ __ __| | ___ _ __ _ _______
// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \
// | | __/ | | | (_| | __/ | | |/ / __/
// \_/\___|_| |_|\__,_|\___|_| |_/___\___|
//
// Copyright (c) Tenderize Labs Ltd

pragma solidity >=0.8.19;

import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { ERC20 } from "solmate/tokens/ERC20.sol";
import { Adapter } from "core/adapters/Adapter.sol";
import { IERC165 } from "core/interfaces/IERC165.sol";
import { ITenderizer } from "core/tenderizer/ITenderizer.sol";
import { IMaticStakeManager, IValidatorShares, DelegatorUnbond } from "core/adapters/interfaces/IPolygon.sol";

// Matic exchange rate precision
uint256 constant EXCHANGE_RATE_PRECISION = 100; // For Validator ID < 8
uint256 constant EXCHANGE_RATE_PRECISION_HIGH = 10 ** 29; // For Validator ID >= 8
uint256 constant WITHDRAW_DELAY = 80; // 80 epochs, epoch length can vary on average between 200-300 Ethereum L1 blocks

// Polygon validators with a `validatorId` less than 8 are foundation validators
// These are special case validators that don't have slashing enabled and still operate
// On the old precision for the ValidatorShares contract.
function getExchangePrecision(uint256 validatorId) pure returns (uint256) {
if (validatorId < 8) {
return EXCHANGE_RATE_PRECISION;
} else {
return EXCHANGE_RATE_PRECISION_HIGH;
}
}

contract PolygonAdapter is Adapter {
using SafeTransferLib for ERC20;

IMaticStakeManager private constant MATIC_STAKE_MANAGER =
IMaticStakeManager(address(0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908));
ERC20 private constant POLY = ERC20(address(0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0));

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

function isValidator(address validator) public view returns (bool) {
// Validator must have a validator shares contract
return address(_getValidatorSharesContract(_getValidatorId(validator))) != address(0);
}

function previewDeposit(uint256 assets) external pure returns (uint256) {
return assets;
}

function previewWithdraw(uint256 unlockID) external view returns (uint256 amount) {
// get validator for caller (Tenderizer through delegate call)
address validator = _getValidatorAddress();
// get the validator shares contract for validator
uint256 validatorId = _getValidatorId(validator);
IValidatorShares validatorShares = _getValidatorSharesContract(validatorId);

DelegatorUnbond memory unbond = validatorShares.unbonds_new(address(this), unlockID);
// calculate amount of tokens to withdraw by converting shares back into amount
// See https://github.com/maticnetwork/contracts/blob/main/contracts/staking/validatorShare/ValidatorShare.sol#L281-L282
amount = unbond.shares * validatorShares.withdrawExchangeRate() / getExchangePrecision(validatorId);
}

function unlockMaturity(uint256 unlockID) external view returns (uint256) {
// Note that this returns the unlockMaturity as a Polygon epoch number in the future
// It's fairly hard to predict the number of blocks between checkpoints
// While we could use an historical average, it's better to just return the epoch number for now
// consumers of this method can still convert it into a block or timestamp if they choose to
DelegatorUnbond memory u =
_getValidatorSharesContract(_getValidatorId(_getValidatorAddress())).unbonds_new(address(this), unlockID);
return u.withdrawEpoch + WITHDRAW_DELAY;
}

function stake(address validator, uint256 amount) external override {
// approve tokens
POLY.safeApprove(address(MATIC_STAKE_MANAGER), amount);

uint256 validatorId = _getValidatorId(validator);
IValidatorShares validatorShares = _getValidatorSharesContract(validatorId);

// calculate minimum amount of voucher shares to mint
// adjust for integer truncation upon division
uint256 precision = getExchangePrecision(validatorId);
uint256 fxRate = validatorShares.exchangeRate();
uint256 min = amount * precision / fxRate - 1;

// Mint voucher shares
validatorShares.buyVoucher(amount, min);
}

function unstake(address validator, uint256 amount) external override returns (uint256 unlockID) {
uint256 validatorId = _getValidatorId(validator);
IValidatorShares validatorShares = _getValidatorSharesContract(validatorId);

uint256 precision = getExchangePrecision(validatorId);
uint256 fxRate = validatorShares.exchangeRate();

// Unbond tokens
// calculate max amount of validator shares to burn
uint256 max = amount * precision / fxRate + 1;
validatorShares.sellVoucher_new(amount, max);

return validatorShares.unbondNonces(address(this));
}

function withdraw(address validator, uint256 unlockID) external override returns (uint256 amount) {
uint256 validatorId = _getValidatorId(validator);
IValidatorShares validatorShares = _getValidatorSharesContract(validatorId);

DelegatorUnbond memory unbond = validatorShares.unbonds_new(address(this), unlockID);
// foundation validators (id < 8) don't have slashing enabled
// see https://github1s.com/maticnetwork/contracts/blob/main/contracts/staking/validatorShare/ValidatorShare.sol#L89-L95
uint256 fxRate = validatorId >= 8 ? validatorShares.withdrawExchangeRate() : EXCHANGE_RATE_PRECISION;
amount = unbond.shares * fxRate / getExchangePrecision(validatorId);

validatorShares.unstakeClaimTokens_new(unlockID);
}

function rebase(address validator, uint256 currentStake) external returns (uint256 newStake) {
uint256 validatorId = _getValidatorId(validator);
IValidatorShares validatorShares = _getValidatorSharesContract(validatorId);

// This call will revert if there are no rewards
// In which case we don't throw, just return the current staked amount.
try validatorShares.restake() { }

Check warning on line 131 in src/adapters/PolygonAdapter.sol

View workflow job for this annotation

GitHub Actions / lint

Code contains empty blocks
catch {
return currentStake;
}

// Read new stake
uint256 shares = validatorShares.balanceOf(address(this));
uint256 precision = getExchangePrecision(validatorId);
uint256 fxRate = validatorShares.exchangeRate();
newStake = shares * fxRate / precision;
}

function _getValidatorAddress() internal view returns (address) {
return ITenderizer(address(this)).validator();
}

function _getValidatorId(address validator) internal view returns (uint256) {
return MATIC_STAKE_MANAGER.getValidatorId(validator);
}

function _getValidatorSharesContract(uint256 validatorId) internal view returns (IValidatorShares) {
return IValidatorShares(MATIC_STAKE_MANAGER.getValidatorContract(validatorId));
}
}
39 changes: 39 additions & 0 deletions src/adapters/interfaces/IPolygon.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2021 Tenderize <[email protected]>

// SPDX-License-Identifier: MIT

pragma solidity >=0.8.19;

struct DelegatorUnbond {
uint256 shares;
uint256 withdrawEpoch;
}

interface IMaticStakeManager {
function getValidatorId(address user) external view returns (uint256);
function getValidatorContract(uint256 validatorId) external view returns (address);
}

interface IValidatorShares {
function owner() external view returns (address);

function restake() external;

function buyVoucher(uint256 _amount, uint256 _minSharesToMint) external;

function sellVoucher_new(uint256 claimAmount, uint256 maximumSharesToBurn) external;

Check warning on line 24 in src/adapters/interfaces/IPolygon.sol

View workflow job for this annotation

GitHub Actions / lint

Function name must be in mixedCase

function unstakeClaimTokens_new(uint256 unbondNonce) external;

Check warning on line 26 in src/adapters/interfaces/IPolygon.sol

View workflow job for this annotation

GitHub Actions / lint

Function name must be in mixedCase

function exchangeRate() external view returns (uint256);

function validatorId() external view returns (uint256);

function balanceOf(address) external view returns (uint256);

function unbondNonces(address) external view returns (uint256);

function withdrawExchangeRate() external view returns (uint256);

function unbonds_new(address, uint256) external view returns (DelegatorUnbond memory);

Check warning on line 38 in src/adapters/interfaces/IPolygon.sol

View workflow job for this annotation

GitHub Actions / lint

Function name must be in mixedCase
}
2 changes: 2 additions & 0 deletions src/tenderizer/ITenderizer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { Tenderizer } from "core/tenderizer/Tenderizer.sol";
* @dev Contains only the necessary API
*/
interface ITenderizer is IERC20 {
function asset() external view returns (IERC20);
function validator() external view returns (address);
function deposit(address receiver, uint256 assets) external returns (uint256);
function unlock(uint256 assets) external returns (uint256 unlockID);
function withdraw(address receiver, uint256 unlockID) external returns (uint256 amount);
Expand Down
95 changes: 95 additions & 0 deletions test/adapters/PolygonAdapter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: MIT
//
// _____ _ _
// |_ _| | | (_)
// | | ___ _ __ __| | ___ _ __ _ _______
// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \
// | | __/ | | | (_| | __/ | | |/ / __/
// \_/\___|_| |_|\__,_|\___|_| |_/___\___|
//
// Copyright (c) Tenderize Labs Ltd

pragma solidity >=0.8.19;

import { Test, stdError } from "forge-std/Test.sol";
import { PolygonAdapter, EXCHANGE_RATE_PRECISION_HIGH, WITHDRAW_DELAY } from "core/adapters/PolygonAdapter.sol";
import { ITenderizer } from "core/tenderizer/ITenderizer.sol";
import { IMaticStakeManager, IValidatorShares, DelegatorUnbond } from "core/adapters/interfaces/IPolygon.sol";
import { AdapterDelegateCall } from "core/adapters/Adapter.sol";

contract PolygonAdapterTest is Test {
using AdapterDelegateCall for PolygonAdapter;

address MATIC_STAKE_MANAGER = 0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908;

Check warning on line 23 in test/adapters/PolygonAdapter.t.sol

View workflow job for this annotation

GitHub Actions / lint

Explicitly mark visibility of state
address validatorShares = vm.addr(1);

Check warning on line 24 in test/adapters/PolygonAdapter.t.sol

View workflow job for this annotation

GitHub Actions / lint

Explicitly mark visibility of state
uint256 validatorId = 8;

Check warning on line 25 in test/adapters/PolygonAdapter.t.sol

View workflow job for this annotation

GitHub Actions / lint

Explicitly mark visibility of state

PolygonAdapter adapter;

function setUp() public {
adapter = new PolygonAdapter();
vm.etch(MATIC_STAKE_MANAGER, bytes("code"));

// Set default mock calls
// set validator to `address(this)`
vm.mockCall(address(this), abi.encodeCall(ITenderizer.validator, ()), abi.encode(address(this)));
// set validator id for `address(this)` to 8 (not a foundation validator)
vm.mockCall(
MATIC_STAKE_MANAGER, abi.encodeCall(IMaticStakeManager.getValidatorId, (address(this))), abi.encode(validatorId)
);
// set validator shares contract for `address(this)` to `validatorShares`
vm.mockCall(
MATIC_STAKE_MANAGER, abi.encodeCall(IMaticStakeManager.getValidatorContract, (validatorId)), abi.encode(validatorShares)
);
}

function test_isValidator() public {
assertTrue(adapter.isValidator(address(this)));
}

function test_previewDeposit() public {
uint256 assets = 100;
uint256 expected = assets;
uint256 actual = adapter.previewDeposit(assets);
assertEq(actual, expected);
}

function testFuzz_previewWithdraw(uint256 shares, uint256 fxRate) public {
fxRate = bound(fxRate, 0.1 ether, 100 ether);
shares = bound(shares, 1 ether, type(uint256).max / fxRate);
uint256 unlockID = 1;
uint256 expected = shares * fxRate / EXCHANGE_RATE_PRECISION_HIGH;

DelegatorUnbond memory unbond = DelegatorUnbond({ shares: shares, withdrawEpoch: 0 });

vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.unbonds_new, (address(this), unlockID)), abi.encode(unbond));
vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.withdrawExchangeRate, ()), abi.encode(fxRate));
uint256 actual = abi.decode(adapter._delegatecall(abi.encodeCall(PolygonAdapter.previewWithdraw, (unlockID))), (uint256));
assertEq(actual, expected);
}

function testFuzz_unlockMaturity(uint256 epoch) public {
vm.assume(epoch <= type(uint256).max - WITHDRAW_DELAY);
uint256 unlockID = 1;
DelegatorUnbond memory unbond = DelegatorUnbond({ shares: 0, withdrawEpoch: epoch });

vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.unbonds_new, (address(this), unlockID)), abi.encode(unbond));
uint256 actual = abi.decode(adapter._delegatecall(abi.encodeCall(PolygonAdapter.unlockMaturity, (unlockID))), (uint256));
assertEq(actual, epoch + WITHDRAW_DELAY);
}

function test_rebase() public {
uint256 currentStake = 100;
uint256 newStake = 200;
vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.exchangeRate, ()), abi.encode(EXCHANGE_RATE_PRECISION_HIGH));
vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.balanceOf, (address(this))), abi.encode(newStake));
vm.mockCallRevert(validatorShares, abi.encodeCall(IValidatorShares.restake, ()), "");
uint256 actual =
abi.decode(adapter._delegatecall(abi.encodeCall(PolygonAdapter.rebase, (address(this), currentStake))), (uint256));
assertEq(actual, currentStake);

vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.restake, ()), abi.encode(true));
actual = abi.decode(adapter._delegatecall(abi.encodeCall(PolygonAdapter.rebase, (address(this), currentStake))), (uint256));
assertEq(actual, newStake);
}
}
Loading