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

Feat: mainnet fork tests #71

Merged
merged 12 commits into from
Dec 15, 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
7 changes: 3 additions & 4 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
[submodule "lib/forge-std"]
branch = "master"
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/prb-test"]
branch = "0.3.1"
path = "lib/prb-test"
Expand All @@ -26,3 +22,6 @@
[submodule "lib/uniswap-v3-core"]
path = lib/uniswap-v3-core
url = https://github.com/tenderize/uniswap-v3-core
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ depth = 100
[rpc_endpoints]
# Uncomment to enable the RPC server
arbitrum_goerli = "${ARBITRUM_GOERLI_RPC}"
arbitrum = "${ARBITRUM_RPC}"
mainnet = "${MAINNET_RPC}"
4 changes: 2 additions & 2 deletions src/adapters/Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { IERC165 } from "core/interfaces/IERC165.sol";
pragma solidity >=0.8.19;

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

function previewWithdraw(uint256 unlockID) external view returns (uint256);

Expand All @@ -24,7 +24,7 @@ interface Adapter is IERC165 {

function currentTime() external view returns (uint256);

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

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

Expand Down
88 changes: 39 additions & 49 deletions src/adapters/GraphAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ pragma solidity >=0.8.19;
import { ERC20 } from "solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { Adapter } from "core/adapters/Adapter.sol";
import { IGraphStaking, IEpochManager } from "core/adapters/interfaces/IGraph.sol";
import { IGraphStaking, IGraphEpochManager } from "core/adapters/interfaces/IGraph.sol";
import { IERC165 } from "core/interfaces/IERC165.sol";

IGraphEpochManager constant GRAPH_EPOCHS = IGraphEpochManager(0x5A843145c43d328B9bB7a4401d94918f131bB281);
IGraphStaking constant GRAPH_STAKING = IGraphStaking(0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03);
ERC20 constant GRT = ERC20(0x9623063377AD1B27544C965cCd7342f7EA7e88C7);
uint256 constant MAX_PPM = 1e6;

contract GraphAdapter is Adapter {
using SafeTransferLib for ERC20;

IGraphStaking private constant GRAPH = IGraphStaking(0xF55041E37E12cD407ad00CE2910B8269B01263b9);
IEpochManager private constant GRAPH_EPOCHS = IEpochManager(0x03541c5cd35953CD447261122F93A5E7b812D697);
ERC20 private constant GRT = ERC20(0xc944E90C64B2c07662A292be6244BDf05Cda44a7);
uint256 private constant MAX_PPM = 1e6;

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

error WithdrawPending();

Expand All @@ -45,7 +45,6 @@ contract GraphAdapter is Adapter {
uint256 lastEpochUnlockedAt;
mapping(uint256 => Epoch) epochs;
mapping(uint256 => Unlock) unlocks;
uint256 tokensPerShare;
}

function _loadStorage() internal pure returns (Storage storage $) {
Expand All @@ -61,8 +60,12 @@ contract GraphAdapter is Adapter {
return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId;
}

function previewDeposit(uint256 assets) external view override returns (uint256) {
return assets - assets * GRAPH.delegationTaxPercentage() / MAX_PPM;
function previewDeposit(address validator, uint256 assets) external view override returns (uint256) {
assets -= assets * GRAPH_STAKING.delegationTaxPercentage() / MAX_PPM;
IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);

uint256 shares = delPool.tokens != 0 ? assets * delPool.shares / delPool.tokens : assets;
return shares * (delPool.tokens + assets) / (delPool.shares + shares);
}

function previewWithdraw(uint256 unlockID) external view override returns (uint256) {
Expand All @@ -75,7 +78,7 @@ contract GraphAdapter is Adapter {
function unlockMaturity(uint256 unlockID) external view override returns (uint256) {
Storage storage $ = _loadStorage();
Unlock memory unlock = $.unlocks[unlockID];
uint256 THAWING_PERIOD = GRAPH.thawingPeriod();
uint256 THAWING_PERIOD = GRAPH_STAKING.thawingPeriod();
// if userEpoch == currentEpoch, it is yet to unlock
// => unlockBlock + thawingPeriod
// if userEpoch == currentEpoch - 1, it is processing
Expand All @@ -93,16 +96,22 @@ contract GraphAdapter is Adapter {
}

function unlockTime() external view override returns (uint256) {
return GRAPH.thawingPeriod();
return GRAPH_STAKING.thawingPeriod();
}

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

function stake(address validator, uint256 amount) external override {
GRT.safeApprove(address(GRAPH), amount);
GRAPH.delegate(validator, amount);
function isValidator(address validator) public view override returns (bool) {
return GRAPH_STAKING.hasStake(validator);
}

function stake(address validator, uint256 amount) external override returns (uint256) {
GRT.safeApprove(address(GRAPH_STAKING), amount);
uint256 delShares = GRAPH_STAKING.delegate(validator, amount);
IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);
return delShares * delPool.tokens / delPool.shares;
}

function unstake(address validator, uint256 amount) external override returns (uint256 unlockID) {
Expand Down Expand Up @@ -144,62 +153,43 @@ contract GraphAdapter is Adapter {

function rebase(address validator, uint256 currentStake) external override returns (uint256 newStake) {
Storage storage $ = _loadStorage();
Epoch memory currentEpoch = $.epochs[$.currentEpoch];
IGraphStaking.DelegationPool memory delPool = GRAPH.delegationPools(validator);
uint256 currentEpochNum = $.currentEpoch;
Epoch memory currentEpoch = $.epochs[currentEpochNum];
IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);

uint256 _tokensPerShare = delPool.shares != 0 ? delPool.tokens * 1 ether / delPool.shares : 1 ether;
newStake = currentStake;

// Account for rounding error of -1 or +1
// This occurs due to a slight change in ratio because of new delegations or withdrawals,
// rather than an effective reward or loss
if (
(_tokensPerShare >= $.tokensPerShare && _tokensPerShare - $.tokensPerShare <= 1)
|| (_tokensPerShare < $.tokensPerShare && $.tokensPerShare - _tokensPerShare <= 1)
) {
return newStake;
}

IGraphStaking.Delegation memory delegation = GRAPH.getDelegation(validator, address(this));
uint256 staked = delegation.shares * _tokensPerShare / 1 ether;
IGraphStaking.Delegation memory delegation = GRAPH_STAKING.getDelegation(validator, address(this));
uint256 staked = delegation.shares * delPool.tokens / delPool.shares;

// account for stake still to unstake
uint256 oldStake = currentStake + currentEpoch.amount;

// Last epoch amount should be synced with Delegation.tokensLocked
if ($.currentEpoch > 0) $.epochs[$.currentEpoch - 1].amount = delegation.tokensLocked;
if (currentEpochNum > 0) $.epochs[currentEpochNum - 1].amount = delegation.tokensLocked;

if (staked > oldStake) {
// handle rewards
// To reduce long waiting periods we want to still reward users
// for which their stake is still to be unlocked
// because technically it is not unlocked from the Graph either
// We do this by adding the rewards to the current epoch
uint256 currentEpochAmount = (staked - oldStake) * currentEpoch.amount / oldStake;
currentEpoch.amount += currentEpochAmount;
} else {
return newStake;
currentEpoch.amount += (staked - oldStake) * currentEpoch.amount / oldStake;
$.epochs[currentEpochNum].amount = currentEpoch.amount;
}

$.epochs[$.currentEpoch] = currentEpoch;
$.tokensPerShare = _tokensPerShare;

// slash/rewards is already accounted for in $.epochs[$.currentEpoch].amount
// rewards is already accounted for in $.epochs[$.currentEpoch].amount
newStake = staked - currentEpoch.amount;
}

function isValidator(address validator) public view override returns (bool) {
return GRAPH.hasStake(validator);
}

function _processWithdrawals(address validator) internal {
// process possible withdrawals before unstakes
_processWithdraw(validator);
_processUnstake(validator);
}

function _processUnstake(address validator) internal {
IGraphStaking.Delegation memory del = GRAPH.getDelegation(validator, address(this));
IGraphStaking.Delegation memory del = GRAPH_STAKING.getDelegation(validator, address(this));
// undelegation already ungoing: no-op
if (del.tokensLockedUntil != 0) return;

Expand All @@ -219,13 +209,13 @@ contract GraphAdapter is Adapter {
$.lastEpochUnlockedAt = block.number;

// calculate shares to undelegate from The Graph
uint256 undelegationShares = currentEpochAmount * 1 ether / $.tokensPerShare;

IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);
uint256 undelegationShares = currentEpochAmount * delPool.shares / delPool.tokens;
// account for possible rounding error
undelegationShares = del.shares < undelegationShares ? del.shares : undelegationShares;

// undelegate
GRAPH.undelegate(validator, undelegationShares);
GRAPH_STAKING.undelegate(validator, undelegationShares);
} else if ($.epochs[$.currentEpoch - 1].amount != 0) {
++$.currentEpoch;
$.lastEpochUnlockedAt = block.number;
Expand All @@ -234,7 +224,7 @@ contract GraphAdapter is Adapter {

function _processWithdraw(address validator) internal {
// withdrawal isn't ready: no-op
uint256 tokensLockedUntil = GRAPH.getDelegation(validator, address(this)).tokensLockedUntil;
uint256 tokensLockedUntil = GRAPH_STAKING.getDelegation(validator, address(this)).tokensLockedUntil;
if (tokensLockedUntil == 0 || tokensLockedUntil > GRAPH_EPOCHS.currentEpoch()) return;

Storage storage $ = _loadStorage();
Expand All @@ -244,7 +234,7 @@ contract GraphAdapter is Adapter {
// $.currentEpoch - 1 is safe as we only call this function after at least 1 _processUnstake
// which increments $.currentEpoch, otherwise del.tokensLockedUntil would still be 0 and we would
// not reach this branch
$.epochs[$.currentEpoch - 1].amount = GRAPH.withdrawDelegated(validator, address(0));
$.epochs[$.currentEpoch - 1].amount = GRAPH_STAKING.withdrawDelegated(validator, address(0));
}
}
}
64 changes: 34 additions & 30 deletions src/adapters/LivepeerAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import { IWETH9 } from "core/adapters/interfaces/IWETH9.sol";
import { IERC165 } from "core/interfaces/IERC165.sol";
import { TWAP } from "core/utils/TWAP.sol";

ILivepeerBondingManager constant LIVEPEER_BONDING = ILivepeerBondingManager(0x35Bcf3c30594191d53231E4FF333E8A770453e40);
ILivepeerRoundsManager constant LIVEPEER_ROUNDS = ILivepeerRoundsManager(0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f);
ERC20 constant LPT = ERC20(0x289ba1701C2F088cf0faf8B3705246331cB8A839);
IWETH9 constant WETH = IWETH9(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
ISwapRouter constant UNISWAP_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address constant UNI_POOL = 0x4fD47e5102DFBF95541F64ED6FE13d4eD26D2546;
uint24 constant UNISWAP_POOL_FEE = 3000;
uint256 constant ETH_THRESHOLD = 1e16; // 0.01 ETH
uint32 constant TWAP_INTERVAL = 36_000;

contract LivepeerAdapter is Adapter {
using SafeTransferLib for ERC20;

Expand All @@ -39,26 +49,16 @@ contract LivepeerAdapter is Adapter {
}
}

ILivepeerBondingManager private constant LIVEPEER = ILivepeerBondingManager(0x35Bcf3c30594191d53231E4FF333E8A770453e40);
ILivepeerRoundsManager private constant LIVEPEER_ROUNDS = ILivepeerRoundsManager(0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f);
ERC20 private constant LPT = ERC20(0x289ba1701C2F088cf0faf8B3705246331cB8A839);
IWETH9 private constant WETH = IWETH9(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
ISwapRouter private constant UNISWAP_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address private constant UNI_POOL = 0x4fD47e5102DFBF95541F64ED6FE13d4eD26D2546;
uint24 private constant UNISWAP_POOL_FEE = 10_000;
uint256 private constant ETH_THRESHOLD = 1e16; // 0.01 ETH
uint32 private constant TWAP_INTERVAL = 36_000;

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

function previewDeposit(uint256 assets) external pure returns (uint256) {
function previewDeposit(address, /*validator*/ uint256 assets) external pure returns (uint256) {
return assets;
}

function previewWithdraw(uint256 unlockID) external view returns (uint256 amount) {
(amount,) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID);
(amount,) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID);
}

function unlockMaturity(uint256 unlockID) external view returns (uint256 maturity) {
Expand All @@ -67,48 +67,49 @@ contract LivepeerAdapter is Adapter {
// roundLength = n
// currentRound = r
// withdrawRound = w
// blockRemainingInCurrentRound = b = roundLength - (block.number - currentRoundStartBlock)
// blocksRemainingInCurrentRound = b = roundLength - (block.number - currentRoundStartBlock)
// maturity = n*(w - r - 1) + b
(, uint256 withdrawRound) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID);
(, uint256 withdrawRound) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID);
uint256 currentRound = LIVEPEER_ROUNDS.currentRound();
uint256 roundLength = LIVEPEER_ROUNDS.roundLength();
uint256 currentRoundStartBlock = LIVEPEER_ROUNDS.currentRoundStartBlock();
uint256 blockRemainingInCurrentRound = roundLength - (block.number - currentRoundStartBlock);
uint256 blocksRemainingInCurrentRound = roundLength - (block.number - currentRoundStartBlock);
if (withdrawRound > currentRound) {
maturity = roundLength * (withdrawRound - currentRound - 1) + blockRemainingInCurrentRound;
maturity = block.number + roundLength * (withdrawRound - currentRound - 1) + blocksRemainingInCurrentRound;
}
}

function unlockTime() external view override returns (uint256) {
return LIVEPEER_ROUNDS.roundLength() * LIVEPEER.unbondingPeriod();
return LIVEPEER_ROUNDS.roundLength() * LIVEPEER_BONDING.unbondingPeriod();
}

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

function stake(address validator, uint256 amount) public {
LPT.approve(address(LIVEPEER), amount);
LIVEPEER.bond(amount, validator);
function stake(address validator, uint256 amount) public returns (uint256) {
LPT.safeApprove(address(LIVEPEER_BONDING), amount);
LIVEPEER_BONDING.bond(amount, validator);
return amount;
}

function unstake(address, /*validator*/ uint256 amount) external returns (uint256 unlockID) {
// returns the *next* Livepeer unbonding lock ID for the delegator
// this will be the `unlockID` after calling unbond
(,,,,,, unlockID) = LIVEPEER.getDelegator(address(this));
LIVEPEER.unbond(amount);
(,,,,,, unlockID) = LIVEPEER_BONDING.getDelegator(address(this));
LIVEPEER_BONDING.unbond(amount);
}

function withdraw(address, /*validator*/ uint256 unlockID) external returns (uint256 amount) {
(amount,) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID);
LIVEPEER.withdrawStake(unlockID);
(amount,) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID);
LIVEPEER_BONDING.withdrawStake(unlockID);
}

function rebase(address validator, uint256 currentStake) external returns (uint256 newStake) {
uint256 currentRound = LIVEPEER_ROUNDS.currentRound();

Storage storage $ = _loadStorage();
if ($.lastRebaseRound < currentRound) {
if ($.lastRebaseRound == currentRound) {
return currentStake;
}

Expand All @@ -123,28 +124,31 @@ contract LivepeerAdapter is Adapter {
}

// Read new stake
newStake = LIVEPEER.pendingStake(address(this), 0);
newStake = LIVEPEER_BONDING.pendingStake(address(this), 0);
}

function isValidator(address validator) public view override returns (bool) {
return LIVEPEER.isRegisteredTranscoder(validator);
return LIVEPEER_BONDING.isRegisteredTranscoder(validator);
}

/// @notice function for swapping ETH fees to LPT
function _livepeerClaimFees() internal {
// get pending fees
uint256 pendingFees;
if ((pendingFees = LIVEPEER.pendingFees(address(this), 0)) < ETH_THRESHOLD) return;
if ((pendingFees = LIVEPEER_BONDING.pendingFees(address(this), 0)) < ETH_THRESHOLD) return;

if (!LIVEPEER_ROUNDS.currentRoundInitialized()) return;

// withdraw fees
LIVEPEER.withdrawFees(payable(address(this)), pendingFees);
LIVEPEER_BONDING.withdrawFees(payable(address(this)), pendingFees);
// get ETH balance
uint256 ethBalance = address(this).balance;
// convert fees to WETH
WETH.deposit{ value: ethBalance }();
ERC20(address(WETH)).safeApprove(address(UNISWAP_ROUTER), ethBalance);
// Calculate Slippage Threshold
uint256 twapPrice = TWAP.getInversePriceX96(TWAP.getPriceX96(TWAP.getSqrtTwapX96(UNI_POOL, TWAP_INTERVAL)));
uint160 sqrtPriceLimitX96 = TWAP.getSqrtTwapX96(UNI_POOL, TWAP_INTERVAL);
uint256 twapPrice = TWAP.getInversePriceX96(TWAP.getPriceX96(sqrtPriceLimitX96));
uint256 amountOut = ethBalance * twapPrice >> 96;
// Create initial params for swap
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
Expand Down
Loading
Loading