Contract Name:
CartographerElevation
Contract Source Code:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.2;
import "./Cartographer.sol";
import "./ElevationHelper.sol";
import "./interfaces/ISubCart.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
/*
---------------------------------------------------------------------------------------------
-- S U M M I T . D E F I
---------------------------------------------------------------------------------------------
Summit is highly experimental.
It has been crafted to bring a new flavor to the defi world.
We hope you enjoy the Summit.defi experience.
If you find any bugs in these contracts, please claim the bounty (see docs)
Created with love by Architect and the Summit team
---------------------------------------------------------------------------------------------
-- Y I E L D G A M B L I N G E X P L A N A T I O N
---------------------------------------------------------------------------------------------
Funds are staked in elevation farms, and the resulting yield is risked to earn a higher yield multiplier
The staked funds are safe from risk, and cannot ever be lost
STAKING:
. 3 tiers exist: PLAINS / MESA / SUMMIT
. Each tier has a set of TOTEMs
. Users select a totem to represent them at the 'multiplying table', shared by all pools at that elevation
. Funds are staked / withdrawn in the same way as traditional pools / farms, represented by their selected totem
. Over time, a user's BET builds up as traditional staking does
. Instead of the staking yield being immediately available, it is risked against the yields of other stakers
. BETs build over the duration of a ROUND
. The summed BETs of all users is considered the POT for that round
ROUNDS:
. At the end of each round, the round is ROLLED OVER
. The ROLLOVER selects a TOTEM as the winner for that round
. All users represented by that TOTEM are considered winners of that round
. The winning TOTEM wins the entire pot
. Winning users split the whole pot, effectively earning the staking rewards of the other users
. Winnings vest over the duration of the next round
---------------------------------------------------------------------------------------------
-- Y I E L D G A M B L I N G C A L C U L A T I O N S O V E R V I E W
---------------------------------------------------------------------------------------------
POOL:
. At the end of each round, during the 'rollover' process, the following is saved in `poolRoundInfo` to be used in user's winnings calculations:
- endAccSummitPerShare - the accSummitPerShare when the round ended
- winningsMultiplier - how much each user's yield reward is multiplied by: (pool roundRewards) / (pool winning totem roundRewards)
- precomputedFullRoundMult - (the change in accSummitPerShare over the whole round) * (winningsMultiplier)
USER:
. The user's funds can be left in a pool over multiple rounds without any interaction
. On the fly calculation of all previous rounds winnings (if any) must be fast and efficient
. Any user interaction with a pool updates the following in UserInfo:
- user.prevInteractedRound - Marks that the current round is the user last interaction with this pool
- user.staked - Amount staked in the pool
- user.roundDebt - The current accSummitPerShare, used to calculate the rewards earned by the user from the current mid-round point, to the end of the round
- user.roundRew - The user may interact with the same round multiple times without losing any existing farmed rewards, this stores any accumulated rewards that have built up mid round, and is increased with each subsequent round interaction in the same round
*/
contract CartographerElevation is ISubCart, Initializable, ReentrancyGuard {
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.AddressSet;
// ---------------------------------------
// -- V A R I A B L E S
// ---------------------------------------
Cartographer public cartographer;
ElevationHelper public elevationHelper;
uint8 public elevation;
address public summitTokenAddress;
struct UserInfo {
// Yield Multiplying
uint256 prevInteractedRound; // Round the user last made a deposit / withdrawal / claim
uint256 staked; // The amount of token the user has in the pool
uint256 roundDebt; // Used to calculate user's first interacted round reward
uint256 roundRew; // Running sum of user's rewards earned in current round
uint256 winningsDebt; // AccWinnings of user's totem at time of deposit
}
struct UserElevationInfo {
address userAdd;
uint8 totem;
bool totemSelected;
uint256 totemSelectionRound;
}
mapping(address => EnumerableSet.AddressSet) userInteractingPools;
struct RoundInfo {
uint256 endAccSummitPerShare; // The accSummitPerShare at the end of the round, used for back calculations
uint256 winningsMultiplier; // Rewards multiplier: TOTAL POOL STAKED / WINNING TOTEM STAKED
uint256 precomputedFullRoundMult; // Gas optimization for back calculation: accSummitPerShare over round multiplied by winnings multiplier
}
struct ElevationPoolInfo {
address token; // Address of reward token contract
bool launched; // If the start round of the pool has passed and it is open for staking / rewards
bool live; // If the pool is running, in lieu of allocPoint
bool active; // Whether the pool is active, used to keep pool alive until round rollover
uint256 lastRewardTimestamp; // Last timestamp that SUMMIT distribution occurs.
uint256 accSummitPerShare; // Accumulated SUMMIT per share, raised 1e12. See below.
uint256 supply; // Running total of the token amount staked in this pool at elevation
uint256[] totemSupplies; // Running total of LP in each totem to calculate rewards
uint256 roundRewards; // Rewards of entire pool accum over round
uint256[] totemRoundRewards; // Rewards of each totem accum over round
uint256[] totemRunningPrecomputedMult; // Running winnings per share for each totem
}
EnumerableSet.AddressSet private poolTokens;
EnumerableSet.AddressSet private activePools;
mapping(address => ElevationPoolInfo) public poolInfo; // Pool info for each elevation pool
mapping(address => mapping(uint256 => RoundInfo)) public poolRoundInfo; // The round end information for each round of each pool
mapping(address => mapping(address => UserInfo)) public userInfo; // Users running staking / vesting information
mapping(address => UserElevationInfo) public userElevationInfo;// User's totem info at each elevation
mapping(uint256 => uint256) public roundWinningsMult;
// ---------------------------------------
// -- A D M I N I S T R A T I O N
// ---------------------------------------
/// @dev Constructor, setting address of cartographer
constructor(address _Cartographer, uint8 _elevation)
{
require(_Cartographer != address(0), "Cartographer required");
require(_elevation >= 1 && _elevation <= 3, "Invalid elevation");
cartographer = Cartographer(_Cartographer);
elevation = _elevation;
}
/// @dev Set address of ElevationHelper during initialization
function initialize(address _ElevationHelper, address _summitTokenAddress)
external override
initializer onlyCartographer
{
require(_ElevationHelper != address(0), "Contract is zero");
require(_summitTokenAddress != address(0), "SummitToken is zero");
elevationHelper = ElevationHelper(_ElevationHelper);
summitTokenAddress = _summitTokenAddress;
}
/// @dev Unused enable summit stub
function enable(uint256) external override {}
// ------------------------------------------------------
// -- M O D I F I E R S
// ------------------------------------------------------
function _onlyCartographer() internal view {
require(msg.sender == address(cartographer), "Only cartographer");
}
modifier onlyCartographer() {
_onlyCartographer();
_;
}
function _totemSelected(address _userAdd) internal view returns (bool) {
return userElevationInfo[_userAdd].totemSelected;
}
modifier userHasSelectedTotem(address _userAdd) {
require(_totemSelected(_userAdd), "Totem not selected");
_;
}
function _validUserAdd(address _userAdd) internal pure {
require(_userAdd != address(0), "User address is zero");
}
modifier validUserAdd(address _userAdd) {
_validUserAdd(_userAdd);
_;
}
modifier nonDuplicated(address _token) {
require(!poolTokens.contains(_token), "Duplicated");
_;
}
modifier validTotem(uint8 _totem) {
require(_totem < elevationHelper.totemCount(elevation), "Invalid totem");
_;
}
modifier elevationTotemSelectionAvailable() {
require(!elevationHelper.endOfRoundLockoutActive(elevation) || elevationHelper.elevationLocked(elevation), "Totem selection locked");
_;
}
function _elevationInteractionsAvailable() internal view {
require(!elevationHelper.endOfRoundLockoutActive(elevation), "Elev locked until rollover");
}
modifier elevationInteractionsAvailable() {
_elevationInteractionsAvailable();
_;
}
function _poolExists(address _token) internal view {
require(poolTokens.contains(_token), "Pool doesnt exist");
}
modifier poolExists(address _token) {
_poolExists(_token);
_;
}
// ---------------------------------------
// -- U T I L S (inlined for brevity)
// ---------------------------------------
function supply(address _token) external view override returns (uint256) {
return poolInfo[_token].supply;
}
function _getUserTotem(address _userAdd) internal view returns (uint8) {
return userElevationInfo[_userAdd].totem;
}
function isTotemSelected(address _userAdd) external view override returns (bool) {
return _totemSelected(_userAdd);
}
function userStakedAmount(address _token, address _userAdd) external view override returns (uint256) {
return userInfo[_token][_userAdd].staked;
}
function getUserInteractingPools(address _userAdd) public view returns (address[] memory) {
return userInteractingPools[_userAdd].values();
}
function getPools() public view returns (address[] memory) {
return poolTokens.values();
}
function getActivePools() public view returns (address[] memory) {
return activePools.values();
}
function totemSupplies(address _token) public view poolExists(_token) returns (uint256[] memory) {
return poolInfo[_token].totemSupplies;
}
/// @dev Calculate the emission to bring the selected pool current
function emissionToBringPoolCurrent(ElevationPoolInfo memory pool) internal view returns (uint256) {
// Early escape if pool is already up to date or not live
if (block.timestamp == pool.lastRewardTimestamp || pool.supply == 0 || !pool.live || !pool.launched) return 0;
// Get the (soon to be) awarded summit emission for this pool over the timespan that would bring current
return cartographer.poolSummitEmission(pool.lastRewardTimestamp, pool.token, elevation);
}
/// @dev Calculates up to date pool round rewards and totem round rewards with pool's emission
/// @param _token Pool identifier
/// @return Up to date versions of round rewards.
/// [poolRoundRewards, ...totemRoundRewards]
function totemRoundRewards(address _token)
public view
poolExists(_token)
returns (uint256[] memory)
{
ElevationPoolInfo storage pool = poolInfo[_token];
uint8 totemCount = elevationHelper.totemCount(elevation);
uint256[] memory roundRewards = new uint256[](totemCount + 1);
// Gets emission that would bring the pool current from last reward timestamp
uint256 emissionToBringCurrent = emissionToBringPoolCurrent(pool);
// Add total emission to bring current to pool round rewards
roundRewards[0] = pool.roundRewards + emissionToBringCurrent;
// For each totem, increase round rewards proportionally to amount staked in that totem compared to full pool's amount staked
for (uint8 i = 0; i < totemCount; i++) {
// If pool or totem doesn't have anything staked, the totem's round rewards won't change with the new emission
if (pool.supply == 0 || pool.totemSupplies[i] == 0) {
roundRewards[i + 1] = pool.totemRoundRewards[i];
// Increase the totem's round rewards with a proportional amount of the new emission
} else {
roundRewards[i + 1] = pool.totemRoundRewards[i] + (emissionToBringCurrent * pool.totemSupplies[i] / pool.supply);
}
}
// Return up to date round rewards
return roundRewards;
}
// ---------------------------------------
// -- P O O L M A N A G E M E N T
// ---------------------------------------
function _markPoolActive(ElevationPoolInfo storage pool, bool _active)
internal
{
if (pool.active == _active) return;
require(!_active || (activePools.length() < 24), "Too many active pools");
pool.active = _active;
if (_active) {
activePools.add(pool.token);
} else {
activePools.remove(pool.token);
}
}
/// @dev Creates a new elevation yield multiplying pool
/// @param _token Token of the pool (also identifier)
/// @param _live Whether the pool is enabled initially
/// @param _token Token yielded by pool
function add(address _token, bool _live)
external override
onlyCartographer nonDuplicated(_token)
{
// Register pool token
poolTokens.add(_token);
// Create the initial state of the elevation pool
poolInfo[_token] = ElevationPoolInfo({
token: _token,
launched: false,
live: _live,
active: false, // Will be made active in the add active pool below if _live is true
supply: 0,
accSummitPerShare : 0,
lastRewardTimestamp : block.timestamp,
totemSupplies : new uint256[](elevationHelper.totemCount(elevation)),
roundRewards : 0,
totemRoundRewards : new uint256[](elevationHelper.totemCount(elevation)),
totemRunningPrecomputedMult: new uint256[](elevationHelper.totemCount(elevation))
});
if (_live) _markPoolActive(poolInfo[_token], true);
}
/// @dev Update a given pools deposit or live status
/// @param _token Pool token
/// @param _live If pool is available for staking
function set(address _token, bool _live)
external override
onlyCartographer poolExists(_token)
{
ElevationPoolInfo storage pool = poolInfo[_token];
updatePool(_token);
// If pool is live, add to active pools list
if (_live) _markPoolActive(pool, true);
// Else pool is becoming inactive, which will be reflected at the end of the round in pool rollover function
// Update IsEarning in Cartographer
_updateTokenIsEarning(pool);
// Update internal pool states
pool.live = _live;
}
/// @dev Mark whether this token is earning at this elevation in the Cartographer
/// Active must be true
/// Launched must be true
/// Staked supply must be non zero
function _updateTokenIsEarning(ElevationPoolInfo storage pool)
internal
{
cartographer.setIsTokenEarningAtElevation(
pool.token,
elevation,
pool.active && pool.launched && pool.supply > 0
);
}
/// @dev Update all pools to current timestamp before other pool management transactions
function massUpdatePools()
external override
onlyCartographer
{
for (uint16 index = 0; index < poolTokens.length(); index++) {
updatePool(poolTokens.at(index));
}
}
/// @dev Bring reward variables of given pool current
/// @param _token Pool token
function updatePool(address _token)
public
poolExists(_token)
{
ElevationPoolInfo storage pool = poolInfo[_token];
// Early exit if the pool is already current
if (pool.lastRewardTimestamp == block.timestamp) return;
// Early exit if pool not launched, not live, or supply is 0
// Timestamp still updated before exit to prevent over emission on return to live
if (!pool.launched || pool.supply == 0 || !pool.live) {
pool.lastRewardTimestamp = block.timestamp;
return;
}
// Mint Summit according to time delta, pools token share and elevation, and tokens allocation share
uint256 summitReward = cartographer.poolSummitEmission(pool.lastRewardTimestamp, pool.token, elevation);
// Update accSummitPerShare with amount of summit minted for pool
pool.accSummitPerShare += summitReward * 1e12 / pool.supply;
// Update the overall pool summit rewards for the round (used in winnings multiplier at end of round)
pool.roundRewards += summitReward;
// Update each totem's summit rewards for the round (used in winnings multiplier at end of round)
for (uint8 i = 0; i < pool.totemRoundRewards.length; i++) {
pool.totemRoundRewards[i] += summitReward * pool.totemSupplies[i] / pool.supply;
}
// Update last reward timestamp
pool.lastRewardTimestamp = block.timestamp;
}
// ---------------------------------------
// -- P O O L R E W A R D S
// ---------------------------------------
/// @dev Fetch claimable yield rewards amount of the pool
/// @param _token Pool token to fetch rewards from
/// @param _userAdd User requesting rewards info
/// @return claimableRewards - Amount of Summit available to claim
function poolClaimableRewards(address _token, address _userAdd)
public view
poolExists(_token) validUserAdd(_userAdd)
returns (uint256)
{
ElevationPoolInfo storage pool = poolInfo[_token];
UserInfo storage user = userInfo[_token][_userAdd];
// Return claimable winnings
return _claimableWinnings(pool, user, _userAdd);
}
/// @dev Fetch claimable yield rewards amount of the elevation
/// @param _userAdd User requesting rewards info
/// @return elevClaimableRewards: Amount of Summit available to claim across the elevation
function elevClaimableRewards(address _userAdd)
public view
validUserAdd(_userAdd)
returns (uint256)
{
// Claim rewards of users active pools
uint256 claimable = 0;
// Iterate through pools the user is interacting, get claimable amount, update pool
address[] memory interactingPools = userInteractingPools[_userAdd].values();
for (uint8 index = 0; index < interactingPools.length; index++) {
// Claim winnings
claimable += _claimableWinnings(
poolInfo[interactingPools[index]],
userInfo[interactingPools[index]][_userAdd],
_userAdd
);
}
return claimable;
}
/// @dev The user's yield generated across their active pools at this elevation, and the hypothetical winnings based on that yield
/// @param _userAdd User to sum and calculate
/// @return (
/// elevationYieldContributed - Total yieldContributed across all pools of this elevation
/// elevationPotentialWinnings - Total potential winnings from that yield for the user's selected totem
/// )
function elevPotentialWinnings(address _userAdd)
public view
validUserAdd(_userAdd)
returns (uint256, uint256)
{
// Early exit if user hasn't selected their totem yet
if (!userElevationInfo[_userAdd].totemSelected) return (0, 0);
uint8 userTotem = userElevationInfo[_userAdd].totem;
// Iterate through active pools of elevation, sums {users yield contributed, total rewards earned (all totems), and winning totems's rewards}
uint256 userTotalYieldContributed = 0;
uint256 elevTotalRewards = 0;
uint256 userTotemTotalWinnings = 0;
for (uint16 index = 0; index < activePools.length(); index++) {
// Add live round rewards of pool and winning totem to elevation round reward accumulators
(uint256 poolUserYieldContributed, uint256 poolRewards, uint256 poolUserTotemWinnings) = _liveUserAndPoolRoundRewards(
activePools.at(index),
userTotem,
_userAdd
);
userTotalYieldContributed += poolUserYieldContributed;
elevTotalRewards += poolRewards;
userTotemTotalWinnings += poolUserTotemWinnings;
}
// Calculate the winnings multiplier of the users totem (assuming it wins)
uint256 elevWinningsMult = userTotemTotalWinnings == 0 ? 0 : elevTotalRewards * 1e12 / userTotemTotalWinnings;
return (
userTotalYieldContributed,
userTotalYieldContributed * elevWinningsMult / 1e12
);
}
/// @dev The user's yield generated and contributed to this elevations round pot from a specific pool
/// @param _token Pool token to check
/// @param _userAdd User to check
/// @return The yield from staking, which has been contributed during the current round
function poolYieldContributed(address _token, address _userAdd)
public view
poolExists(_token) validUserAdd(_userAdd)
returns (uint256)
{
// Calculate current accSummitPerShare to bring the pool current
(uint256 accSummitPerShare,) = _liveAccSummitPerShare(poolInfo[_token]);
// Return yield generated hypothetical yield (what the user would have earned if this was standard staking) with brought current accSummitPerShare
return _liveYieldContributed(
poolInfo[_token],
userInfo[_token][_userAdd],
accSummitPerShare
);
}
/// @dev Calculates the accSummitPerShare if the pool was brought current and awarded summit emissions
/// @param pool Elevation pool
/// @return (
/// liveAccSummitPerShare - What the current accSummitPerShare of the pool would be if brought current
/// summitEmission - The awarded emission to the pool that would bring it current
/// )
function _liveAccSummitPerShare(ElevationPoolInfo memory pool)
internal view
returns (uint256, uint256)
{
// Calculate emission to bring the pool up to date
uint256 emissionToBringCurrent = emissionToBringPoolCurrent(pool);
// Calculate the new accSummitPerShare with the emission to bring current, and return both values
return (
pool.accSummitPerShare + (pool.supply == 0 ? 0 : (emissionToBringCurrent * 1e12 / pool.supply)),
emissionToBringCurrent
);
}
/// @dev The staking rewards that would be earned during the current round under standard staking conditions
/// @param pool Pool info
/// @param user User info
/// @param accSummitPerShare Is brought current before call
function _liveYieldContributed(ElevationPoolInfo memory pool, UserInfo storage user, uint256 accSummitPerShare)
internal view
returns (uint256)
{
uint256 currRound = elevationHelper.roundNumber(elevation);
return user.prevInteractedRound == currRound ?
// Change in accSummitPerShare from current timestamp to users previous interaction timestamp (factored into user.roundDebt)
(user.staked * accSummitPerShare / 1e12) - user.roundDebt + user.roundRew :
// Change in accSummitPerShare from current timestamp to beginning of round's timestamp (stored in previous round's endAccRewPerShare)
user.staked * (accSummitPerShare - poolRoundInfo[pool.token][currRound - 1].endAccSummitPerShare) / 1e12;
}
/// @dev Brings the pool's round rewards, and the user's selected totem's round rewards current
/// Round rewards are the total amount of yield generated by a pool over the duration of a round
/// totemRoundRewards is the total amount of yield generated by the funds staked in each totem of the pool
/// @param _token Pool token identifier
/// @param _userTotem User's totem, gas saving
/// @param _userAdd User's address to return yield generated totem to bring current
/// @return (
/// userPoolYieldContributed - How much yield the user has contributed to the pot from this pool
/// liveRoundRewards - The brought current round rewards of the pool
/// liveUserTotemRoundRewards - The brought current round rewards of the user's selected totem
/// )
function _liveUserAndPoolRoundRewards(address _token, uint8 _userTotem, address _userAdd)
internal view
returns (uint256, uint256, uint256)
{
ElevationPoolInfo storage pool = poolInfo[_token];
(uint256 liveAccSummitPerShare, uint256 emissionToBringCurrent) = _liveAccSummitPerShare(pool);
return (
// User's yield generated
userInteractingPools[_userAdd].contains(_token) ? _liveYieldContributed(
poolInfo[_token],
userInfo[_token][_userAdd],
liveAccSummitPerShare
) : 0,
// Round rewards with the total emission to bring current added
pool.roundRewards + emissionToBringCurrent,
// Calculate user's totem's round rewards
pool.supply == 0 || pool.totemSupplies[_userTotem] == 0 ?
// Early exit with current round rewards of user's totem if pool or user's totem has 0 supply (would cause div/0 error)
pool.totemRoundRewards[_userTotem] :
// Add the proportion of total emission that would be granted to the user's selected totem to that totem's round rewards
// Proportion of total emission earned by each totem is (totem's staked supply / pool's staked supply)
pool.totemRoundRewards[_userTotem] + (((emissionToBringCurrent * 1e12 * pool.totemSupplies[_userTotem]) / pool.supply) / 1e12)
);
}
// ------------------------------------------------------------------
// -- R O L L O V E R E L E V A T I O N R O U N D
// ------------------------------------------------------------------
/// @dev Sums the total rewards and winning totem rewards from each pool and determines the elevations winnings multiplier, then rolling over all active pools
function rollover()
external override
onlyCartographer
{
uint256 prevRound = elevationHelper.roundNumber(elevation) == 0 ? 0 : elevationHelper.roundNumber(elevation) - 1;
uint8 winningTotem = elevationHelper.winningTotem(elevation, prevRound);
// Iterate through active pools of elevation, sum total rewards earned (all totems), and winning totems's rewards
uint256 elevTotalRewards = 0;
uint256 winningTotemRewards = 0;
address[] memory pools = activePools.values();
for (uint16 index = 0; index < pools.length; index++) {
// Bring pool current
updatePool(pools[index]);
// Add round rewards of pool and winning totem to elevation round reward accumulators
elevTotalRewards += poolInfo[pools[index]].roundRewards;
winningTotemRewards += poolInfo[pools[index]].totemRoundRewards[winningTotem];
}
// Calculate the winnings multiplier of the round that just ended from the combined reward amounts
roundWinningsMult[prevRound] = winningTotemRewards == 0 ? 0 : elevTotalRewards * 1e12 / winningTotemRewards;
// Update and rollover all active pools
for (uint16 index = 0; index < pools.length; index++) {
// Rollover Pool
rolloverPool(pools[index], prevRound, roundWinningsMult[prevRound]);
}
}
/// @dev Roll over a single pool and create a new poolRoundInfo entry
/// @param _token Pool to roll over
/// @param _prevRound Round index of the round that just ended
/// @param _winningsMultiplier Winnings mult of the winning totem based on rewards of entire elevation
function rolloverPool(address _token, uint256 _prevRound, uint256 _winningsMultiplier)
internal
{
ElevationPoolInfo storage pool = poolInfo[_token];
// Remove pool from active pool list if it has been marked for removal
if (!pool.live) _markPoolActive(pool, false);
// Launch pool if it hasn't been, early exit since it has no earned rewards before launch
if (!pool.launched) {
pool.launched = true;
_updateTokenIsEarning(pool);
return;
}
// The change in accSummitPerShare from the end of the previous round to the end of the current round
uint256 deltaAccSummitPerShare = pool.accSummitPerShare - poolRoundInfo[_token][_prevRound - 1].endAccSummitPerShare;
uint256 precomputedFullRoundMult = deltaAccSummitPerShare * _winningsMultiplier / 1e12;
// Increment running precomputed mult with previous round's data
pool.totemRunningPrecomputedMult[elevationHelper.winningTotem(elevation, _prevRound)] += precomputedFullRoundMult;
// Adding a new entry to the pool's poolRoundInfo for the most recently closed round
poolRoundInfo[_token][_prevRound] = RoundInfo({
endAccSummitPerShare: pool.accSummitPerShare,
winningsMultiplier: _winningsMultiplier,
precomputedFullRoundMult: precomputedFullRoundMult
});
// Resetting round reward accumulators to begin accumulating over the next round
pool.roundRewards = 0;
pool.totemRoundRewards = new uint256[](elevationHelper.totemCount(elevation));
}
// ------------------------------------------------------------
// -- W I N N I N G S C A L C U L A T I O N S
// ------------------------------------------------------------
/// @dev Calculation of round rewards of the first round interacted
/// @param user Users staking info
/// @param round Passed in instead of used inline in this function to prevent stack too deep error
/// @param _totem Totem to determine if round was won and winnings warranted
/// @return Winnings from round
function _userFirstInteractedRoundWinnings(UserInfo storage user, RoundInfo memory round, uint8 _totem)
internal view
returns (uint256)
{
if (_totem != elevationHelper.winningTotem(elevation, user.prevInteractedRound)) return 0;
return ((user.staked * round.endAccSummitPerShare / 1e12) - user.roundDebt + user.roundRew) * round.winningsMultiplier / 1e12;
}
/// @dev Calculation of winnings that are available to be claimed
/// @param pool Pool info
/// @param user UserInfo
/// @param _userAdd User's address passed through for win check
/// @return Total claimable winnings for a user, including vesting on previous round's winnings (if any)
function _claimableWinnings(ElevationPoolInfo storage pool, UserInfo storage user, address _userAdd)
internal view
returns (uint256)
{
uint256 currRound = elevationHelper.roundNumber(elevation);
// Exit early if no previous round exists to have winnings, or user has already interacted this round
if (!pool.launched || user.prevInteractedRound == currRound) return 0;
uint8 totem = _getUserTotem(_userAdd);
uint256 claimable = 0;
// Get winnings from first user interacted round if it was won (requires different calculation)
claimable += _userFirstInteractedRoundWinnings(user, poolRoundInfo[pool.token][user.prevInteractedRound], totem);
// Escape early if user interacted during previous round
if (user.prevInteractedRound == currRound - 1) return claimable;
// The change in precomputed mult of the user's first interacting round, this value doesn't exist when user.winningsDebt is set, so must be included here
uint256 firstInteractedRoundDeltaPrecomputedMult = totem == elevationHelper.winningTotem(elevation, user.prevInteractedRound) ?
poolRoundInfo[pool.token][user.prevInteractedRound].precomputedFullRoundMult :
0;
uint256 winningsDebtAtEndOfFirstInteractedRound = user.winningsDebt + firstInteractedRoundDeltaPrecomputedMult;
// Add multiple rounds of precomputed mult delta for all rounds between first interacted and most recent round
claimable += user.staked * (pool.totemRunningPrecomputedMult[totem] - winningsDebtAtEndOfFirstInteractedRound) / 1e12;
return claimable;
}
// ------------------------------------------------------------
// -- W I N N I N G S I N T E R A C T I O N S
// ------------------------------------------------------------
/// @dev Claim any available winnings, and
/// @param pool Pool info
/// @param user User info
/// @param _userAdd USer's address used for redeeming rewards and checking for if rounds won
function _claimWinnings(ElevationPoolInfo storage pool, UserInfo storage user, address _userAdd)
internal
returns (uint256)
{
// Get user's winnings available for claim
uint256 claimable = _claimableWinnings(pool, user, _userAdd);
// Claim winnings if any available, return claimed amount with bonuses applied
if (claimable > 0) {
return cartographer.claimWinnings(_userAdd, pool.token, claimable);
}
return claimable;
}
/// @dev Update the users round interaction
/// @param pool Pool info
/// @param user User info
/// @param _totem Totem (potentially new totem)
/// @param _amount Amount depositing / withdrawing
/// @param _isDeposit Flag to differentiate deposit / withdraw
function _updateUserRoundInteraction(ElevationPoolInfo storage pool, UserInfo storage user, uint8 _totem, uint256 _amount, bool _isDeposit)
internal
{
uint256 currRound = elevationHelper.roundNumber(elevation);
// User already interacted this round, update the current round reward by adding staking rewards between two interactions this round
if (user.prevInteractedRound == currRound) {
user.roundRew += (user.staked * pool.accSummitPerShare / 1e12) - user.roundDebt;
// User has no staked value, create a fresh round reward
} else if (user.staked == 0) {
user.roundRew = 0;
// User interacted in some previous round, create a fresh round reward based on the current staked amount's staking rewards from the beginning of this round to the current point
} else {
// The accSummitPerShare at the beginning of this round. This is known to exist because a user has already interacted in a previous round
uint256 roundStartAccSummitPerShare = poolRoundInfo[pool.token][currRound - 1].endAccSummitPerShare;
// Round rew is the current staked amount * delta accSummitPerShare from the beginning of the round until now
user.roundRew = user.staked * (pool.accSummitPerShare - roundStartAccSummitPerShare) / 1e12;
}
// Update the user's staked amount with either the deposit or withdraw amount
if (_isDeposit) user.staked += _amount;
else user.staked -= _amount;
// Fresh calculation of round debt from the new staked amount
user.roundDebt = user.staked * pool.accSummitPerShare / 1e12;
// Acc Winnings Per Share of the user's totem
user.winningsDebt = pool.totemRunningPrecomputedMult[_totem];
// Update the user's previous interacted round to be this round
user.prevInteractedRound = currRound;
}
// ------------------------------------------------------------
// -- E L E V A T I O N T O T E M S
// ------------------------------------------------------------
/// @dev Increments or decrements user's pools at elevation staked, and adds to / removes from users list of staked pools
function _markUserInteractingWithPool(address _token, address _userAdd, bool _interacting) internal {
// Early escape if interacting state already up to date
if (userInteractingPools[_userAdd].contains(_token) == _interacting) return;
// Validate staked pool cap
require(!_interacting || userInteractingPools[_userAdd].length() < 12, "Staked pool cap (12) reached");
if (_interacting) {
userInteractingPools[_userAdd].add(_token);
} else {
userInteractingPools[_userAdd].remove(_token);
}
}
/// @dev All funds at an elevation share a totem. This function allows switching staked funds from one totem to another
/// @param _totem New target totem
/// @param _userAdd User requesting switch
function switchTotem(uint8 _totem, address _userAdd)
external override
nonReentrant onlyCartographer validTotem(_totem) validUserAdd(_userAdd) elevationTotemSelectionAvailable
{
uint8 prevTotem = _getUserTotem(_userAdd);
// Early exit if totem is same as current
require(!_totemSelected(_userAdd) || prevTotem != _totem, "Totem must change");
// Iterate through pools the user is interacting with and update totem
uint256 claimable = 0;
address[] memory interactingPools = userInteractingPools[_userAdd].values();
for (uint8 index = 0; index < interactingPools.length; index++) {
claimable += _switchTotemForPool(interactingPools[index], prevTotem, _totem, _userAdd);
}
// Update user's totem in state
userElevationInfo[_userAdd].totem = _totem;
userElevationInfo[_userAdd].totemSelected = true;
userElevationInfo[_userAdd].totemSelectionRound = elevationHelper.roundNumber(elevation);
}
/// @dev Switch users funds (if any staked) to the new totem
/// @param _token Pool identifier
/// @param _prevTotem Totem the user is leaving
/// @param _newTotem Totem the user is moving to
/// @param _userAdd User doing the switch
function _switchTotemForPool(address _token, uint8 _prevTotem, uint8 _newTotem, address _userAdd)
internal
returns (uint256)
{
UserInfo storage user = userInfo[_token][_userAdd];
ElevationPoolInfo storage pool = poolInfo[_token];
uint256 claimable = _unifiedClaim(
pool,
user,
_newTotem,
_userAdd
);
// Transfer supply and round rewards from previous totem to new totem
pool.totemSupplies[_prevTotem] -= user.staked;
pool.totemSupplies[_newTotem] += user.staked;
pool.totemRoundRewards[_prevTotem] -= user.roundRew;
pool.totemRoundRewards[_newTotem] += user.roundRew;
return claimable;
}
// ------------------------------------------------------------
// -- P O O L I N T E R A C T I O N S
// ------------------------------------------------------------
/// @dev User interacting with pool getter
function _userInteractingWithPool(UserInfo storage user)
internal view
returns (bool)
{
return (user.staked + user.roundRew) > 0;
}
/// @dev Claim an entire elevation's winnings
/// @param _userAdd User claiming
function claimElevation(address _userAdd)
external override
validUserAdd(_userAdd) elevationInteractionsAvailable onlyCartographer
returns (uint256)
{
// Claim rewards of users active pools
uint256 claimable = 0;
// Iterate through pools the user is interacting, get claimable amount, update pool
address[] memory interactingPools = userInteractingPools[_userAdd].values();
for (uint8 index = 0; index < interactingPools.length; index++) {
// Claim winnings
claimable += _unifiedClaim(
poolInfo[interactingPools[index]],
userInfo[interactingPools[index]][_userAdd],
_getUserTotem(_userAdd),
_userAdd
);
}
return claimable;
}
/// @dev Wrapper around cartographer token management on deposit
function _depositTokenManagement(address _token, uint256 _amount, address _userAdd)
internal
returns (uint256)
{
return cartographer.depositTokenManagement(_userAdd, _token, _amount);
}
function _depositValidate(address _token, address _userAdd)
internal view
userHasSelectedTotem(_userAdd) poolExists(_token) validUserAdd(_userAdd) elevationInteractionsAvailable
{ return; }
/// @dev Stake funds in a yield multiplying elevation pool
/// @param _token Pool to stake in
/// @param _amount Amount to stake
/// @param _userAdd User wanting to stake
/// @param _isElevate Whether this is the deposit half of an elevate tx
/// @return Amount deposited after deposit fee taken
function deposit(address _token, uint256 _amount, address _userAdd, bool _isElevate)
external override
nonReentrant onlyCartographer
returns (uint256)
{
// User has selected their totem, pool exists, user is valid, elevation is open for interactions
_depositValidate(_token, _userAdd);
// Claim earnings from pool
_unifiedClaim(
poolInfo[_token],
userInfo[_token][_userAdd],
_getUserTotem(_userAdd),
_userAdd
);
// Deposit amount into pool
return _unifiedDeposit(
poolInfo[_token],
userInfo[_token][_userAdd],
_amount,
_userAdd,
_isElevate
);
}
/// @dev Emergency Withdraw reset user
/// @param pool Pool info
/// @param _userAdd USer's address used for redeeming rewards and checking for if rounds won
function _emergencyWithdrawResetUser(ElevationPoolInfo storage pool, address _userAdd) internal {
userInfo[pool.token][_userAdd] = UserInfo({
roundRew: 0,
staked: 0,
roundDebt: 0,
winningsDebt: 0,
prevInteractedRound: 0
});
}
/// @dev Emergency withdraw without rewards
/// @param _token Pool to emergency withdraw from
/// @param _userAdd User emergency withdrawing
/// @return Amount emergency withdrawn
function emergencyWithdraw(address _token, address _userAdd)
external override
nonReentrant onlyCartographer poolExists(_token) validUserAdd(_userAdd)
returns (uint256)
{
ElevationPoolInfo storage pool = poolInfo[_token];
UserInfo storage user = userInfo[_token][_userAdd];
uint256 staked = user.staked;
// Reset User Info
_emergencyWithdrawResetUser(pool, _userAdd);
// Signal cartographer to perform withdrawal function
uint256 amountAfterFee = cartographer.withdrawalTokenManagement(_userAdd, _token, staked);
// Remove withdrawn amount from pool's running supply accumulators
pool.totemSupplies[_getUserTotem(_userAdd)] -= staked;
pool.supply -= staked;
// If the user is interacting with this pool after the meat of the transaction completes
_markUserInteractingWithPool(_token, _userAdd, false);
// Return amount withdraw
return amountAfterFee;
}
/// @dev Withdraw staked funds from pool
/// @param _token Pool to withdraw from
/// @param _amount Amount to withdraw
/// @param _userAdd User withdrawing
/// @param _isElevate Whether this is the withdraw half of an elevate tx
/// @return True amount withdrawn
function withdraw(address _token, uint256 _amount, address _userAdd, bool _isElevate)
external override
nonReentrant onlyCartographer poolExists(_token) validUserAdd(_userAdd) elevationInteractionsAvailable
returns (uint256)
{
// Claim earnings from pool
_unifiedClaim(
poolInfo[_token],
userInfo[_token][_userAdd],
_getUserTotem(_userAdd),
_userAdd
);
// Withdraw amount from pool
return _unifiedWithdraw(
poolInfo[_token],
userInfo[_token][_userAdd],
_amount,
_userAdd,
_isElevate
);
}
/// @dev Claim winnings from a pool
/// @param pool ElevationPoolInfo of pool to withdraw from
/// @param user UserInfo of withdrawing user
/// @param _totem Users Totem (new totem if necessary)
/// @param _userAdd User address
/// @return Amount claimable
function _unifiedClaim(ElevationPoolInfo storage pool, UserInfo storage user, uint8 _totem, address _userAdd)
internal
returns (uint256)
{
updatePool(pool.token);
// Claim available winnings, returns claimed amount with bonuses applied
uint256 claimable = _claimWinnings(pool, user, _userAdd);
// Update the users round interaction, may be updated again in the same tx, but must be updated here to maintain state
_updateUserRoundInteraction(pool, user, _totem, 0, true);
// Update users pool interaction status, may be updated again in the same tx, but must be updated here to maintain state
_markUserInteractingWithPool(pool.token, _userAdd, _userInteractingWithPool(user));
// Return amount claimed / claimable
return claimable;
}
/// @dev Internal shared deposit functionality for elevate or standard deposit
/// @param pool Pool info of pool to deposit into
/// @param user UserInfo of depositing user
/// @param _amount Amount to deposit
/// @param _userAdd User address
/// @param _isInternalTransfer Flag to switch off certain functionality for elevate deposit
/// @return Amount deposited after fee taken
function _unifiedDeposit(ElevationPoolInfo storage pool, UserInfo storage user, uint256 _amount, address _userAdd, bool _isInternalTransfer)
internal
returns (uint256)
{
updatePool(pool.token);
uint8 totem = _getUserTotem(_userAdd);
uint256 amountAfterFee = _amount;
// Take deposit fee and add to running supplies if amount is non zero
if (_amount > 0) {
// Only take deposit fee on standard deposit
if (!_isInternalTransfer)
amountAfterFee = _depositTokenManagement(pool.token, _amount, _userAdd);
// Adding staked amount to running supply accumulators
pool.totemSupplies[totem] += amountAfterFee;
pool.supply += amountAfterFee;
_updateTokenIsEarning(pool);
}
// Update / create users interaction with the pool
_updateUserRoundInteraction(pool, user, totem, amountAfterFee, true);
// Update users pool interaction status
_markUserInteractingWithPool(pool.token, _userAdd, _userInteractingWithPool(user));
// Return true amount deposited in pool
return amountAfterFee;
}
/// @dev Withdraw functionality shared between standardWithdraw and elevateWithdraw
/// @param pool Pool to withdraw from
/// @param user UserInfo of withdrawing user
/// @param _amount Amount to withdraw
/// @param _userAdd User address
/// @param _isInternalTransfer Flag to switch off certain functionality for elevate withdraw
/// @return Amount withdrawn
function _unifiedWithdraw(ElevationPoolInfo storage pool, UserInfo storage user, uint256 _amount, address _userAdd, bool _isInternalTransfer)
internal
returns (uint256)
{
// Validate amount attempting to withdraw
require(_amount > 0 && user.staked >= _amount, "Bad withdrawal");
uint8 totem = _getUserTotem(_userAdd);
// Bring pool to present
updatePool(pool.token);
// Update the users interaction in the pool
_updateUserRoundInteraction(pool, user, totem, _amount, false);
// Signal cartographer to perform withdrawal function if not elevating funds
// Elevated funds remain in the cartographer, or in the passthrough target, so no need to withdraw from anywhere as they would be immediately re-deposited
uint256 amountAfterFee = _amount;
if (!_isInternalTransfer) {
amountAfterFee = cartographer.withdrawalTokenManagement(_userAdd, pool.token, _amount);
}
// Remove withdrawn amount from pool's running supply accumulators
pool.totemSupplies[totem] -= _amount;
pool.supply -= _amount;
_updateTokenIsEarning(pool);
// If the user is interacting with this pool after the meat of the transaction completes
_markUserInteractingWithPool(pool.token, _userAdd, _userInteractingWithPool(user));
// Return amount withdraw
return amountAfterFee;
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.2;
import "./SummitToken.sol";
import "./CartographerOasis.sol";
import "./CartographerElevation.sol";
import "./EverestToken.sol";
import "./ElevationHelper.sol";
import "./SummitGlacier.sol";
import "./PresetPausable.sol";
import "./libs/SummitMath.sol";
import "./interfaces/ISubCart.sol";
import "./interfaces/IPassthrough.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
/*
---------------------------------------------------------------------------------------------
-- S U M M I T . D E F I
---------------------------------------------------------------------------------------------
Summit is highly experimental.
It has been crafted to bring a new flavor to the defi world.
We hope you enjoy the Summit.defi experience.
If you find any bugs in these contracts, please claim the bounty (see docs)
Created with love by Architect and the Summit team
---------------------------------------------------------------------------------------------
-- S U M M I T E C O S Y S T E M
---------------------------------------------------------------------------------------------
The Cartographer is the anchor of the summit ecosystem
The Cartographer is also the owner of the SUMMIT token
Features of the Summit Ecosystem
- Standard Yield Farming (oasis farms mirror standard farms)
- Yield Multiplying (yield is put into a pot, which allows winning of other user's yield reward)
- Shared token allocation (reward allocation is split by elevation multiplier and amount staked at elevation, to guarantee more rewards at higher elevation)
- Elevating (Deposit once, update strategy without paying tax)
- Decaying Fairness Tax promoting long term staking
- Accruing Loyalty Bonus to earn bonus Summit
- Yield locking for 4 epochs (weeks) in the Glacier
- Passthrough Strategy (to fund expeditions, on oasis and elevation farms)
- Everest to lock SUMMIT and earn ecosystem rewards
- Expedition (daily drawings for Everest holders to earn USDC)
- Random number generation immune to Block Withholding Attack through open source webserver
*/
contract Cartographer is Ownable, Initializable, ReentrancyGuard, PresetPausable {
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.AddressSet;
// ---------------------------------------
// -- V A R I A B L E S
// ---------------------------------------
uint8 constant OASIS = 0;
uint8 constant PLAINS = 1;
uint8 constant MESA = 2;
uint8 constant SUMMIT = 3;
SummitToken public summit;
bool public enabled = false; // Whether the ecosystem has been enabled for earning
uint256 public rolloverReward = 0e18; // Amount of SUMMIT which will be rewarded for rolling over a round
address public treasuryAdd; // Treasury address, see docs for spend breakdown
address public lpGeneratorAdd; // Address that accumulates funds that will be converted into LP
address public expeditionTreasuryAdd; // Expedition Treasury address, intermediate address to convert to stablecoins
ElevationHelper public elevationHelper;
address[4] public subCartographers;
EverestToken public everest;
SummitGlacier public summitGlacier;
uint256 public summitPerSecond = 5e16; // Amount of Summit minted per second to be distributed to users
uint256 public treasurySummitBP = 2000; // Amount of Summit minted per second to the treasury
uint16[4] public elevationPoolsCount; // List of all pool identifiers (PIDs)
mapping(address => address) public tokenPassthroughStrategy; // Passthrough strategy of each stakable token
uint256[4] public elevAlloc; // Total allocation points of all pools at an elevation
EnumerableSet.AddressSet tokensWithAlloc; // List of tokens with an allocation set
mapping(address => uint16) public tokenDepositFee; // Deposit fee for all farms of this token
mapping(address => uint16) public tokenWithdrawalTax; // Tax for all farms of this token
mapping(address => uint256) public tokenAlloc; // A tokens underlying allocation, which is modulated for each elevation
mapping(address => mapping(uint8 => bool)) public poolExistence; // Whether a pool exists for a token at an elevation
mapping(address => mapping(uint8 => bool)) public tokenElevationIsEarning; // If a token is earning SUMMIT at a specific elevation
mapping(address => bool) public isNativeFarmToken;
// First {taxDecayDuration} days from the last withdraw timestamp, no bonus builds. 7 days after that it builds by 1% each day
// Any withdraw resets the bonus to 0% but starts building immediately, which sets the last withdraw timestamp to (current timestamp - {taxDecayDuration})
mapping(address => mapping(address => uint256)) public tokenLastWithdrawTimestampForBonus; // Users' last withdraw timestamp for farm emission bonus
uint256 public maxBonusBP = 700;
mapping(address => mapping(address => uint256)) public tokenLastDepositTimestampForTax; // Users' last deposit timestamp for tax
uint16 public baseMinimumWithdrawalTax = 100;
uint256 public taxDecayDuration = 7 * 86400;
uint256 constant public taxResetOnDepositBP = 500;
// ---------------------------------------
// -- E V E N T S
// ---------------------------------------
event SetTokenAllocation(address indexed token, uint256 alloc);
event PoolCreated(address indexed token, uint8 elevation);
event PoolUpdated(address indexed token, uint8 elevation, bool live);
event Deposit(address indexed user, address indexed token, uint8 indexed elevation, uint256 amount);
event ClaimElevation(address indexed user, uint8 indexed elevation, uint256 totalClaimed);
event Rollover(address indexed user, uint256 elevation);
event SwitchTotem(address indexed user, uint8 indexed elevation, uint8 totem);
event Elevate(address indexed user, address indexed token, uint8 sourceElevation, uint8 targetElevation, uint256 amount);
event EmergencyWithdraw(address indexed user, address indexed token, uint8 indexed elevation, uint256 amount);
event Withdraw(address indexed user, address indexed token, uint8 indexed elevation, uint256 amount);
event ElevateAndLockStakedSummit(address indexed user, uint8 indexed elevation, uint256 amount);
event ClaimWinnings(address indexed user, uint256 amount);
event SetExpeditionTreasuryAddress(address indexed user, address indexed newAddress);
event SetTreasuryAddress(address indexed user, address indexed newAddress);
event SetLpGeneratorAddress(address indexed user, address indexed newAddress);
event PassthroughStrategySet(address indexed token, address indexed passthroughStrategy);
event PassthroughStrategyRetired(address indexed token, address indexed passthroughStrategy);
event SetTokenDepositFee(address indexed _token, uint16 _feeBP);
event SetTokenWithdrawTax(address indexed _token, uint16 _taxBP);
event SetTaxDecayDuration(uint256 _taxDecayDuration);
event SetBaseMinimumWithdrawalTax(uint16 _baseMinimumWithdrawalTax);
event SetTokenIsNativeFarm(address indexed _token, bool _isNativeFarm);
event SetMaxBonusBP(uint256 _maxBonusBP);
event SummitOwnershipTransferred(address indexed _summitOwner);
event SetRolloverReward(uint256 _reward);
event SetTotalSummitPerSecond(uint256 _amount);
event SetSummitDistributionBPs(uint256 _treasuryBP);
// ---------------------------------------
// -- A D M I N I S T R A T I O N
// ---------------------------------------
/// @dev Constructor simply setting addresses on creation
constructor(
address _treasuryAdd,
address _expeditionTreasuryAdd,
address _lpGeneratorAdd
) {
require(_treasuryAdd != address(0), "Missing Treasury");
require(_expeditionTreasuryAdd != address(0), "Missing Exped Treasury");
require(_lpGeneratorAdd != address(0), "Missing Lp Generator Add");
treasuryAdd = _treasuryAdd;
expeditionTreasuryAdd = _expeditionTreasuryAdd;
lpGeneratorAdd = _lpGeneratorAdd;
}
/// @dev Initialize, simply setting addresses, these contracts need the Cartographer address so it must be separate from the constructor
function initialize(
address _summit,
address _ElevationHelper,
address _CartographerOasis,
address _CartographerPlains,
address _CartographerMesa,
address _CartographerSummit,
address _everest,
address _summitGlacier
)
external
initializer onlyOwner
{
require(
_summit != address(0) &&
_ElevationHelper != address(0) &&
_CartographerOasis != address(0) &&
_CartographerPlains != address(0) &&
_CartographerMesa != address(0) &&
_CartographerSummit != address(0) &&
_everest != address(0) &&
_summitGlacier != address(0),
"Contract is zero"
);
summit = SummitToken(_summit);
elevationHelper = ElevationHelper(_ElevationHelper);
subCartographers[OASIS] = _CartographerOasis;
subCartographers[PLAINS] = _CartographerPlains;
subCartographers[MESA] = _CartographerMesa;
subCartographers[SUMMIT] = _CartographerSummit;
everest = EverestToken(_everest);
summit.approve(_everest, type(uint256).max);
summitGlacier = SummitGlacier(_summitGlacier);
// Initialize the subCarts with the address of elevationHelper
for (uint8 elevation = OASIS; elevation <= SUMMIT; elevation++) {
_subCartographer(elevation).initialize(_ElevationHelper, address(_summit));
}
}
/// @dev Enabling the summit ecosystem with the true summit token, turning on farming
function enable() external onlyOwner {
// Prevent multiple instances of enabling
require(!enabled, "Already enabled");
enabled = true;
// Setting and propagating the true summit address and launch timestamp
elevationHelper.enable(block.timestamp);
_subCartographer(OASIS).enable(block.timestamp);
}
/// @dev Transferring Summit Ownership - Huge timelock
function migrateSummitOwnership(address _summitOwner)
public
onlyOwner
{
require(_summitOwner != address(0), "Missing Summit Owner");
summit.transferOwnership(_summitOwner);
emit SummitOwnershipTransferred(_summitOwner);
}
/// @dev Updating the dev address, can only be called by the current dev address
/// @param _treasuryAdd New dev address
function setTreasuryAdd(address _treasuryAdd) public {
require(_treasuryAdd != address(0), "Missing address");
require(msg.sender == treasuryAdd, "Forbidden");
treasuryAdd = _treasuryAdd;
emit SetTreasuryAddress(msg.sender, _treasuryAdd);
}
/// @dev Updating the expedition accumulator address
/// @param _expeditionTreasuryAdd New expedition accumulator address
function setExpeditionTreasuryAdd(address _expeditionTreasuryAdd) public onlyOwner {
require(_expeditionTreasuryAdd != address(0), "Missing address");
expeditionTreasuryAdd = _expeditionTreasuryAdd;
emit SetExpeditionTreasuryAddress(msg.sender, _expeditionTreasuryAdd);
}
/// @dev Updating the LP Generator address
/// @param _lpGeneratorAdd New lp Generator address
function setLpGeneratorAdd(address _lpGeneratorAdd) public onlyOwner {
require(_lpGeneratorAdd != address(0), "Missing address");
lpGeneratorAdd = _lpGeneratorAdd;
emit SetLpGeneratorAddress(msg.sender, _lpGeneratorAdd);
}
/// @dev Update the amount of native token equivalent to reward for rolling over a round
function setRolloverReward(uint256 _reward) public onlyOwner {
require(_reward < 10e18, "Exceeds max reward");
rolloverReward = _reward;
emit SetRolloverReward(_reward);
}
/// @dev Updating the total emission of the ecosystem
/// @param _amount New total emission
function setTotalSummitPerSecond(uint256 _amount) public onlyOwner {
// Must be less than 1 SUMMIT per second
require(_amount < 1e18, "Invalid emission");
summitPerSecond = _amount;
emit SetTotalSummitPerSecond(_amount);
}
/// @dev Updating the emission split profile
/// @param _treasuryBP How much extra is minted for the treasury
function setSummitDistributionBPs(uint256 _treasuryBP) public onlyOwner {
// Require dev emission less than 25% of total emission
require(_treasuryBP <= 2500, "Invalid Distributions");
treasurySummitBP = _treasuryBP;
emit SetSummitDistributionBPs(_treasuryBP);
}
// -----------------------------------------------------------------
// -- M O D I F I E R S (Many are split to save contract size)
// -----------------------------------------------------------------
function _onlySubCartographer(address _isSubCartographer) internal view {
require(
_isSubCartographer == subCartographers[OASIS] ||
_isSubCartographer == subCartographers[PLAINS] ||
_isSubCartographer == subCartographers[MESA] ||
_isSubCartographer == subCartographers[SUMMIT],
"Only subCarts"
);
}
modifier onlySubCartographer() {
_onlySubCartographer(msg.sender);
_;
}
modifier nonDuplicated(address _token, uint8 _elevation) {
require(!poolExistence[_token][_elevation], "Duplicated");
_;
}
modifier nonDuplicatedTokenAlloc(address _token) {
require(!tokensWithAlloc.contains(_token), "Duplicated token alloc");
_;
}
modifier tokenAllocExists(address _token) {
require(tokensWithAlloc.contains(_token), "Invalid token alloc");
_;
}
modifier validAllocation(uint256 _allocation) {
require(_allocation <= 10000, "Allocation must be <= 100X");
_;
}
function _poolExists(address _token, uint8 _elevation) internal view {
require(poolExistence[_token][_elevation], "Pool doesnt exist");
}
modifier poolExists(address _token, uint8 _elevation) {
_poolExists(_token, _elevation);
_;
}
// Elevation validation with min and max elevations (inclusive)
function _validElev(uint8 _elevation, uint8 _minElev, uint8 _maxElev) internal pure {
require(_elevation >= _minElev && _elevation <= _maxElev, "Invalid elev");
}
modifier isOasisOrElevation(uint8 _elevation) {
_validElev(_elevation, OASIS, SUMMIT);
_;
}
modifier isElevation(uint8 _elevation) {
_validElev(_elevation, PLAINS, SUMMIT);
_;
}
// Totem
modifier validTotem(uint8 _elevation, uint8 _totem) {
require(_totem < elevationHelper.totemCount(_elevation), "Invalid totem");
_;
}
// ---------------------------------------------------------------
// -- S U B C A R T O G R A P H E R S E L E C T O R
// ---------------------------------------------------------------
function _subCartographer(uint8 _elevation) internal view returns (ISubCart) {
require(_elevation >= OASIS && _elevation <= SUMMIT, "Invalid elev");
return ISubCart(subCartographers[_elevation]);
}
// ---------------------------------------
// -- T O K E N A L L O C A T I O N
// ---------------------------------------
/// @dev Number of existing pools
function poolsCount()
public view
returns (uint256)
{
uint256 count = 0;
for (uint8 elevation = OASIS; elevation <= SUMMIT; elevation++) {
count += elevationPoolsCount[elevation];
}
return count;
}
/// @dev List of tokens with a set allocation
function tokensWithAllocation()
public view
returns (address[] memory)
{
return tokensWithAlloc.values();
}
/// @dev Create / Update the allocation for a token. This modifies existing allocations at each elevation for that token
/// @param _token Token to update allocation for
/// @param _allocation Updated allocation
function setTokenAllocation(address _token, uint256 _allocation)
public
onlyOwner validAllocation(_allocation)
{
// Token is marked as having an existing allocation
tokensWithAlloc.add(_token);
// Update the tokens allocation at the elevations that token is active at
for (uint8 elevation = OASIS; elevation <= SUMMIT; elevation++) {
if (tokenElevationIsEarning[_token][elevation]) {
elevAlloc[elevation] = elevAlloc[elevation] + _allocation - tokenAlloc[_token];
}
}
// Update the token allocation
tokenAlloc[_token] = _allocation;
emit SetTokenAllocation(_token, _allocation);
}
/// @dev Register pool at elevation as live, add to shared alloc
/// @param _token Token of the pool
/// @param _elevation Elevation of the pool
/// @param _isEarning Whether token is earning SUMMIT at elevation
function setIsTokenEarningAtElevation(address _token, uint8 _elevation, bool _isEarning)
external
onlySubCartographer
{
// Early escape if token earning is already up to date
if (tokenElevationIsEarning[_token][_elevation] == _isEarning) return;
// Add the new allocation to the token's shared allocation and total allocation
if (_isEarning) {
elevAlloc[_elevation] += tokenAlloc[_token];
// Remove the existing allocation to the token's shared allocation and total allocation
} else {
elevAlloc[_elevation] -= tokenAlloc[_token];
}
// Mark the token-elevation earning
tokenElevationIsEarning[_token][_elevation] = _isEarning;
}
/// @dev Sets the passthrough strategy for a given token
/// @param _token Token passthrough strategy applies to
/// @param _passthroughStrategy Address of the new passthrough strategy
function setTokenPassthroughStrategy(address _token, address _passthroughStrategy)
public
onlyOwner
{
// Validate that the strategy exists and tokens match
require(_passthroughStrategy != address(0), "Passthrough strategy missing");
require(address(IPassthrough(_passthroughStrategy).token()) == _token, "Token doesnt match passthrough strategy");
_enactTokenPassthroughStrategy(_token, _passthroughStrategy);
}
/// @dev Retire passthrough strategy and return tokens to this contract
/// @param _token Token whose passthrough strategy to remove
function retireTokenPassthroughStrategy(address _token)
public
onlyOwner
{
require(tokenPassthroughStrategy[_token] != address(0), "No passthrough strategy to retire");
address retiredTokenPassthroughStrategy = tokenPassthroughStrategy[_token];
_retireTokenPassthroughStrategy(_token);
emit PassthroughStrategyRetired(address(_token), retiredTokenPassthroughStrategy);
}
function _enactTokenPassthroughStrategy(address _token, address _passthroughStrategy)
internal
{
// If strategy already exists for this pool, retire from it
_retireTokenPassthroughStrategy(_token);
// Deposit funds into new passthrough strategy
IPassthrough(_passthroughStrategy).token().approve(_passthroughStrategy, type(uint256).max);
IPassthrough(_passthroughStrategy).enact();
// Set token passthrough strategy in state
tokenPassthroughStrategy[_token] = _passthroughStrategy;
emit PassthroughStrategySet(address(_token), _passthroughStrategy);
}
/// @dev Internal functionality of retiring a passthrough strategy
function _retireTokenPassthroughStrategy(address _token) internal {
// Early exit if token doesn't have passthrough strategy
if(tokenPassthroughStrategy[_token] == address(0)) return;
IPassthrough(tokenPassthroughStrategy[_token]).retire(expeditionTreasuryAdd, treasuryAdd, lpGeneratorAdd);
tokenPassthroughStrategy[_token] = address(0);
}
// ---------------------------------------
// -- P O O L M A N A G E M E N T
// ---------------------------------------
/// @dev Creates a new pool for a token at a specific elevation
/// @param _token Token to create the pool for
/// @param _elevation The elevation to create this pool at
/// @param _live Whether the pool is available for staking (independent of rounds / elevation constraints)
/// @param _withUpdate Whether to update all pools during this transaction
function add(address _token, uint8 _elevation, bool _live, bool _withUpdate)
public
onlyOwner tokenAllocExists(_token) isOasisOrElevation(_elevation) nonDuplicated(_token, _elevation)
{
// Mass update if required
if (_withUpdate) {
massUpdatePools();
}
// Get the next available pool identifier and register pool
poolExistence[_token][_elevation] = true;
elevationPoolsCount[_elevation] += 1;
// Create the pool in the appropriate sub cartographer
_subCartographer(_elevation).add(_token, _live);
emit PoolCreated(_token, _elevation);
}
/// @dev Update pool's live status and deposit tax
/// @param _token Pool identifier
/// @param _elevation Elevation of pool
/// @param _live Whether staking is permitted on this pool
/// @param _withUpdate whether to update all pools as part of this transaction
function set(address _token, uint8 _elevation, bool _live, bool _withUpdate)
public
onlyOwner isOasisOrElevation(_elevation) poolExists(_token, _elevation)
{
// Mass update if required
if (_withUpdate) {
massUpdatePools();
}
// Updates the pool in the correct subcartographer
_subCartographer(_elevation).set(_token, _live);
emit PoolUpdated(_token, _elevation, _live);
}
/// @dev Does what it says on the box
function massUpdatePools() public {
for (uint8 elevation = OASIS; elevation <= SUMMIT; elevation++) {
_subCartographer(elevation).massUpdatePools();
}
}
// ------------------------------------------------------------------
// -- R O L L O V E R E L E V A T I O N R O U N D
// ------------------------------------------------------------------
/// @dev Rolling over a round for an elevation and selecting winning totem.
/// Called by the webservice, but can also be called manually by any user (as failsafe)
/// @param _elevation Elevation to rollover
function rollover(uint8 _elevation)
public whenNotPaused
nonReentrant isElevation(_elevation)
{
// Ensure that the elevation is ready to be rolled over, ensures only a single user can perform the rollover
elevationHelper.validateRolloverAvailable(_elevation);
// Selects the winning totem for the round, storing it in the elevationHelper contract
elevationHelper.selectWinningTotem(_elevation);
// Update the round index in the elevationHelper, effectively starting the next round of play
elevationHelper.rolloverElevation(_elevation);
// Rollover active pools at the elevation
_subCartographer(_elevation).rollover();
// Give SUMMIT rewards to user that executed the rollover
summit.mint(msg.sender, rolloverReward);
emit Rollover(msg.sender, _elevation);
}
// -----------------------------------------------------
// -- S U M M I T E M I S S I O N
// -----------------------------------------------------
/// @dev Returns the modulated allocation of a token at elevation, escaping early if the pool is not live
/// @param _token Tokens allocation
/// @param _elevation Elevation to modulate
/// @return True allocation of the pool at elevation
function elevationModulatedAllocation(address _token, uint8 _elevation) public view returns (uint256) {
// Escape early if the pool is not currently earning SUMMIT
if (!tokenElevationIsEarning[_token][_elevation]) return 0;
// Fetch the modulated base allocation for the token at elevation
return elevationHelper.elevationModulatedAllocation(tokenAlloc[_token], _elevation);
}
/// @dev Shares of a token at elevation
/// (@param _token, @param _elevation) Together identify the pool to calculate
function _tokenElevationShares(address _token, uint8 _elevation) internal view returns (uint256) {
// Escape early if the pool doesn't exist or is not currently earning SUMMIT
if (!poolExistence[_token][_elevation] || !tokenElevationIsEarning[_token][_elevation]) return 0;
return (
_subCartographer(_elevation).supply(_token) *
elevationModulatedAllocation(_token, _elevation)
);
}
/// @dev The share of the total token at elevation emission awarded to the pool
/// Tokens share allocation to ensure that staking at higher elevation ALWAYS has higher APY
/// This is done to guarantee a higher ROI at higher elevations over a long enough time span
/// The allocation of each pool is based on the elevation, as well as the staked supply at that elevation
/// @param _token The token (+ elevation) to evaluate emission for
/// @param _elevation The elevation (+ token) to evaluate emission for
/// @return The share of emission granted to the pool, raised to 1e12
function tokenElevationEmissionMultiplier(address _token, uint8 _elevation)
public view
returns (uint256)
{
// Shares for all elevation are summed. For each elevation the shares are calculated by:
// . The staked supply of the pool at elevation multiplied by
// . The modulated allocation of the pool at elevation
uint256 totalTokenShares = (
_tokenElevationShares(_token, OASIS) +
_tokenElevationShares(_token, PLAINS) +
_tokenElevationShares(_token, MESA) +
_tokenElevationShares(_token, SUMMIT)
);
// Escape early if nothing is staked in any of the token's pools
if (totalTokenShares == 0) return 0;
// Divide the target pool (token + elevation) shares by total shares (as calculated above)
return _tokenElevationShares(_token, _elevation) * 1e12 / totalTokenShares;
}
/// @dev Emission multiplier of token based on its allocation
/// @return Multiplier raised 1e12
function tokenAllocEmissionMultiplier(address _token)
public view
returns (uint256)
{
// Sum allocation of all elevations with allocation multipliers
uint256 tokenTotalAlloc = 0;
uint256 totalAlloc = 0;
for (uint8 elevation = OASIS; elevation <= SUMMIT; elevation++) {
if (tokenElevationIsEarning[_token][elevation]) {
tokenTotalAlloc += tokenAlloc[_token] * elevationHelper.elevationAllocMultiplier(elevation);
}
totalAlloc += elevAlloc[elevation] * elevationHelper.elevationAllocMultiplier(elevation);
}
if (totalAlloc == 0) return 0;
return tokenTotalAlloc * 1e12 / totalAlloc;
}
/// @dev Uses the tokenElevationEmissionMultiplier along with timeDiff and token allocation to calculate the overall emission multiplier of the pool
/// @param _lastRewardTimestamp Calculate the difference to determine emission event count
/// (@param _token, @param elevation) Pool identifier for calculation
/// @return Share of overall emission granted to the pool, raised to 1e12
function _poolEmissionMultiplier(uint256 _lastRewardTimestamp, address _token, uint8 _elevation)
internal view
returns (uint256)
{
// Calculate overall emission granted over time span, calculated by:
// . Time difference from last reward timestamp
// . Tokens allocation as a fraction of total allocation
// . Pool's emission multiplier
return (block.timestamp - _lastRewardTimestamp) * tokenAllocEmissionMultiplier(_token) * tokenElevationEmissionMultiplier(_token, _elevation) / 1e12;
}
/// @dev Uses _poolEmissionMultiplier along with staking summit emission to calculate the pools summit emission over the time span
/// @param _lastRewardTimestamp Used for time span
/// (@param _token, @param _elevation) Pool identifier
/// @return emission of SUMMIT, not raised to any power
function poolSummitEmission(uint256 _lastRewardTimestamp, address _token, uint8 _elevation)
external view
onlySubCartographer
returns (uint256)
{
// Escape early if no time has passed
if (block.timestamp <= _lastRewardTimestamp) { return 0; }
// Emission multiplier multiplied by summitPerSecond, finally reducing back to true exponential
return _poolEmissionMultiplier(_lastRewardTimestamp, _token, _elevation) * summitPerSecond / 1e12;
}
// -----------------------------------------------------
// -- S W I T C H T O T E M
// -----------------------------------------------------
/// @dev All funds at an elevation share a totem. This function allows switching staked funds from one totem to another
/// @param _elevation Elevation to switch totem on
/// @param _totem New target totem
function switchTotem(uint8 _elevation, uint8 _totem)
public whenNotPaused
nonReentrant isElevation(_elevation) validTotem(_elevation, _totem)
{
// Executes the totem switch in the correct subcartographer
_subCartographer(_elevation).switchTotem(_totem, msg.sender);
emit SwitchTotem(msg.sender, _elevation, _totem);
}
// -----------------------------------------------------
// -- P O O L I N T E R A C T I O N S
// -----------------------------------------------------
/// @dev Get the user's tax for a token
/// @param _userAdd user address
/// @param _token token address
function taxBP(address _userAdd, address _token)
public view
returns (uint16)
{
return _getTaxBP(_userAdd, _token);
}
function _getTaxBP(address _userAdd, address _token)
public view
returns (uint16)
{
uint256 lastDepositTimestampForTax = tokenLastDepositTimestampForTax[_userAdd][_token];
uint256 tokenTax = uint256(tokenWithdrawalTax[_token]);
uint256 tokenMinTax = isNativeFarmToken[_token] ? 0 : baseMinimumWithdrawalTax;
// Early exit if min tax is greater than tax of this token
if (tokenMinTax >= tokenTax) return uint16(tokenMinTax);
return uint16(SummitMath.scaledValue(
block.timestamp,
lastDepositTimestampForTax, lastDepositTimestampForTax + taxDecayDuration,
tokenTax, tokenMinTax
));
}
/// @dev Get bonus BP
/// @param _userAdd user address
/// @param _token token address
function bonusBP(address _userAdd, address _token)
public view
returns (uint16)
{
return _getBonusBP(_userAdd, _token);
}
function _getBonusBP(address _userAdd, address _token)
public view
returns (uint16)
{
uint256 lastWithdrawTimestamp = tokenLastWithdrawTimestampForBonus[_userAdd][_token];
// Early exit if last Withdraw Timestamp hasn't ever been ste
if (lastWithdrawTimestamp == 0) return 0;
return uint16(SummitMath.scaledValue(
block.timestamp,
lastWithdrawTimestamp + taxDecayDuration, lastWithdrawTimestamp + (taxDecayDuration * 2),
0, maxBonusBP
));
}
/// @dev Users staked amount across all elevations
/// @param _token Token to determine user's staked amount of
function userTokenStakedAmount(address _userAdd, address _token)
public view
returns (uint256)
{
return _userTokenStakedAmount(_token, _userAdd);
}
function _userTokenStakedAmount(address _token, address _userAdd)
internal view
returns (uint256)
{
uint256 totalStaked = 0;
for (uint8 elevation = OASIS; elevation <= SUMMIT; elevation++) {
totalStaked += _subCartographer(elevation).userStakedAmount(_token, _userAdd);
}
return totalStaked;
}
/// @dev Stake funds with a pool, is also used to claim a single farm with a deposit amount of 0
/// (@param _token, @param _elevation) Pool identifier
/// @param _amount Amount to stake
function deposit(address _token, uint8 _elevation, uint256 _amount)
public whenNotPaused
nonReentrant poolExists(_token, _elevation)
{
// Executes the deposit in the sub cartographer
uint256 amountAfterTax = _subCartographer(_elevation)
.deposit(
_token,
_amount,
msg.sender,
false
);
// Set initial value of token last withdraw timestamp (for bonus) if it hasn't already been set
if (tokenLastWithdrawTimestampForBonus[msg.sender][_token] == 0) {
tokenLastWithdrawTimestampForBonus[msg.sender][_token] = block.timestamp;
}
// Reset tax timestamp if user is depositing greater than {taxResetOnDepositBP}% of current staked amount
if (_amount > (_userTokenStakedAmount(_token, msg.sender) * taxResetOnDepositBP / 10000)) {
tokenLastDepositTimestampForTax[msg.sender][_token] = block.timestamp;
}
emit Deposit(msg.sender, _token, _elevation, amountAfterTax);
}
/// @dev Claim all rewards (or cross compound) of an elevation
/// @param _elevation Elevation to claim all rewards from
function claimElevation(uint8 _elevation)
public whenNotPaused
nonReentrant isOasisOrElevation(_elevation)
{
// Harvest across an elevation, return total amount claimed
uint256 totalClaimed = _subCartographer(_elevation).claimElevation(msg.sender);
emit ClaimElevation(msg.sender, _elevation, totalClaimed);
}
/// @dev Withdraw staked funds from a pool
/// (@param _token, @param _elevation) Pool identifier
function emergencyWithdraw(address _token, uint8 _elevation)
public
nonReentrant poolExists(_token, _elevation)
{
// Executes the withdrawal in the sub cartographer
uint256 amountAfterTax = _subCartographer(_elevation)
.emergencyWithdraw(
_token,
msg.sender
);
// Farm bonus handling, sets the last withdraw timestamp to 7 days ago (tax decay duration) to begin earning bonuses immediately
// Update to the max of (current last withdraw timestamp, current timestamp - 7 days), which ensures the first 7 days are never building bonus
tokenLastWithdrawTimestampForBonus[msg.sender][_token] = Math.max(
tokenLastWithdrawTimestampForBonus[msg.sender][_token],
block.timestamp - taxDecayDuration
);
emit EmergencyWithdraw(msg.sender, _token, _elevation, amountAfterTax);
}
/// @dev Withdraw staked funds from a pool
/// (@param _token, @param _elevation) Pool identifier
/// @param _amount Amount to withdraw, must be > 0 and <= staked amount
function withdraw(address _token, uint8 _elevation, uint256 _amount)
public whenNotPaused
nonReentrant poolExists(_token, _elevation)
{
// Executes the withdrawal in the sub cartographer
uint256 amountAfterTax = _subCartographer(_elevation)
.withdraw(
_token,
_amount,
msg.sender,
false
);
// Farm bonus handling, sets the last withdraw timestamp to 7 days ago (tax decay duration) to begin earning bonuses immediately
// Update to the max of (current last withdraw timestamp, current timestamp - 7 days), which ensures the first 7 days are never building bonus
tokenLastWithdrawTimestampForBonus[msg.sender][_token] = Math.max(
tokenLastWithdrawTimestampForBonus[msg.sender][_token],
block.timestamp - taxDecayDuration
);
emit Withdraw(msg.sender, _token, _elevation, amountAfterTax);
}
/// @dev Elevate SUMMIT from the Elevation farms to the Expedition without paying any withdrawal tax
/// @param _elevation Elevation to elevate from
/// @param _amount Amount of SUMMIT to elevate
function elevateAndLockStakedSummit(uint8 _elevation, uint256 _amount)
public whenNotPaused
nonReentrant poolExists(address(summit), _elevation)
{
require(_amount > 0, "Elevate non zero amount");
// Withdraw {_amount} of {_token} from {_elevation} pool
uint256 elevatedAmount = _subCartographer(_elevation)
.withdraw(
address(summit),
_amount,
msg.sender,
true
);
// Lock withdrawn SUMMIT for EVEREST
everest.lockAndExtendLockDuration(
elevatedAmount,
everest.minLockTime(),
msg.sender
);
emit ElevateAndLockStakedSummit(msg.sender, _elevation, _amount);
}
/// @dev Validation step of Elevate into separate function
/// @param _token Token to elevate
/// @param _sourceElevation Elevation to withdraw from
/// @param _targetElevation Elevation to deposit into
/// @param _amount Amount to elevate
function _validateElevate(address _token, uint8 _sourceElevation, uint8 _targetElevation, uint256 _amount)
internal view
poolExists(_token, _sourceElevation) poolExists(_token, _targetElevation)
{
require(_amount > 0, "Transfer non zero amount");
require(_sourceElevation != _targetElevation, "Must change elev");
require(
_subCartographer(_sourceElevation).isTotemSelected(msg.sender) &&
_subCartographer(_targetElevation).isTotemSelected(msg.sender),
"Totem not selected"
);
}
/// @dev Allows funds to be transferred between elevations without forcing users to pay a deposit tax
/// @param _token Token to elevate
/// @param _sourceElevation Elevation to withdraw from
/// @param _targetElevation Elevation to deposit into
/// @param _amount Amount to elevate
function elevate(address _token, uint8 _sourceElevation, uint8 _targetElevation, uint256 _amount)
public whenNotPaused
nonReentrant
{
_validateElevate(_token, _sourceElevation, _targetElevation, _amount);
// Withdraw {_amount} of {_token} from {_sourceElevation} pool
uint256 elevatedAmount = _subCartographer(_sourceElevation)
.withdraw(
_token,
_amount,
msg.sender,
true
);
// Deposit withdrawn amount of {_token} from source pool {elevatedAmount} into {_targetPid} pool
elevatedAmount = _subCartographer(_targetElevation)
.deposit(
_token,
elevatedAmount,
msg.sender,
true
);
emit Elevate(msg.sender, _token, _sourceElevation, _targetElevation, elevatedAmount);
}
// -----------------------------------------------------
// -- Y I E L D L O C K I N G
// -----------------------------------------------------
/// @dev Utility function to handle claiming Summit rewards with bonuses
/// @return Claimed amount with bonuses included
function claimWinnings(address _userAdd, address _token, uint256 _amount)
external whenNotPaused
onlySubCartographer
returns (uint256)
{
uint256 tokenBonusBP = _getBonusBP(_userAdd, _token);
uint256 bonusWinnings = _amount * tokenBonusBP / 10000;
uint256 totalWinnings = _amount + bonusWinnings;
// Mint Summit user has won, and additional mints for distribution
summit.mint(address(summitGlacier), totalWinnings);
summit.mint(treasuryAdd, totalWinnings * treasurySummitBP / 10000);
// Send users claimable winnings to SummitGlacier.sol
summitGlacier.addLockedWinnings(totalWinnings, bonusWinnings, _userAdd);
emit ClaimWinnings(_userAdd, totalWinnings);
return totalWinnings;
}
// -----------------------------------------------------
// -- T O K E N M A N A G E M E N T
// -----------------------------------------------------
/// @dev Utility function for depositing tokens into passthrough strategy
function _passthroughDeposit(address _token, uint256 _amount) internal returns (uint256) {
if (tokenPassthroughStrategy[_token] == address(0)) return _amount;
return IPassthrough(tokenPassthroughStrategy[_token]).deposit(_amount, expeditionTreasuryAdd, treasuryAdd, lpGeneratorAdd);
}
/// @dev Utility function for withdrawing tokens from passthrough strategy
/// @param _token Token to withdraw from it's passthrough strategy
/// @param _amount Amount requested to withdraw
/// @return The true amount withdrawn from the passthrough strategy after the passthrough's tax was taken (if any)
function _passthroughWithdraw(address _token, uint256 _amount) internal returns (uint256) {
if (tokenPassthroughStrategy[_token] == address(0)) return _amount;
return IPassthrough(tokenPassthroughStrategy[_token]).withdraw(_amount, expeditionTreasuryAdd, treasuryAdd, lpGeneratorAdd);
}
/// @dev Transfers funds from user on deposit
/// @param _userAdd Depositing user
/// @param _token Token to deposit
/// @param _amount Deposit amount before tax
/// @return Deposit amount
function depositTokenManagement(address _userAdd, address _token, uint256 _amount)
external whenNotPaused
onlySubCartographer
returns (uint256)
{
// Transfers total deposit amount
IERC20(_token).safeTransferFrom(_userAdd, address(this), _amount);
// Take and distribute deposit fee
uint256 amountAfterFee = _amount;
if (tokenDepositFee[_token] > 0) {
amountAfterFee = _amount * (10000 - tokenDepositFee[_token]) / 10000;
_distributeTaxesAndFees(_token, _amount * tokenDepositFee[_token] / 10000);
}
// Deposit full amount to passthrough, return amount deposited
return _passthroughDeposit(_token, amountAfterFee);
}
/// @dev Takes the remaining withdrawal tax (difference between total withdrawn amount and the amount expected to be withdrawn after the remaining tax)
/// @param _token Token to withdraw
/// @param _amount Funds above the amount after remaining withdrawal tax that was returned from the passthrough strategy
function _distributeTaxesAndFees(address _token, uint256 _amount)
internal
{
IERC20(_token).safeTransfer(treasuryAdd, _amount / 2);
IERC20(_token).safeTransfer(expeditionTreasuryAdd, _amount / 2);
}
/// @dev Transfers funds to user on withdraw
/// @param _userAdd Withdrawing user
/// @param _token Token to withdraw
/// @param _amount Withdraw amount
/// @return Amount withdrawn after tax
function withdrawalTokenManagement(address _userAdd, address _token, uint256 _amount)
external
onlySubCartographer
returns (uint256)
{
// Withdraw full amount from passthrough (if any), if there is a tax that isn't covered by the increase in vault value this may be less than expected full amount
uint256 amountAfterTax = _passthroughWithdraw(_token, _amount);
// Amount user expects to receive after tax taken
uint256 expectedWithdrawnAmount = (_amount * (10000 - _getTaxBP(_userAdd, _token))) / 10000;
// Take any remaining tax (gap between what was actually withdrawn, and what the user expects to receive)
if (amountAfterTax > expectedWithdrawnAmount) {
_distributeTaxesAndFees(_token, amountAfterTax - expectedWithdrawnAmount);
amountAfterTax = expectedWithdrawnAmount;
}
// Transfer funds back to user
IERC20(_token).safeTransfer(_userAdd, amountAfterTax);
return amountAfterTax;
}
// ---------------------------------------
// -- W I T H D R A W A L T A X
// ---------------------------------------
/// @dev Set the tax for a token
function setTokenDepositFee(address _token, uint16 _feeBP)
public
onlyOwner
{
// Deposit fee will never be higher than 4%
require(_feeBP <= 400, "Invalid fee > 4%");
tokenDepositFee[_token] = _feeBP;
emit SetTokenDepositFee(_token, _feeBP);
}
/// @dev Set the tax for a token
function setTokenWithdrawTax(address _token, uint16 _taxBP)
public
onlyOwner
{
// Taxes will never be higher than 10%
require(_taxBP <= 1000, "Invalid tax > 10%");
tokenWithdrawalTax[_token] = _taxBP;
emit SetTokenWithdrawTax(_token, _taxBP);
}
/// @dev Set the tax decaying duration
function setTaxDecayDuration(uint256 _taxDecayDuration)
public
onlyOwner
{
require(_taxDecayDuration <= 14 days, "Invalid duration > 14d");
taxDecayDuration = _taxDecayDuration;
emit SetTaxDecayDuration(_taxDecayDuration);
}
/// @dev Set the minimum withdrawal tax
function setBaseMinimumWithdrawalTax(uint16 _baseMinimumWithdrawalTax)
public
onlyOwner
{
require(_baseMinimumWithdrawalTax <= 1000, "Minimum tax outside 0%-10%");
baseMinimumWithdrawalTax = _baseMinimumWithdrawalTax;
emit SetBaseMinimumWithdrawalTax(_baseMinimumWithdrawalTax);
}
/// @dev Set whether a token is a native farm
function setTokenIsNativeFarm(address _token, bool _isNativeFarm)
public
onlyOwner
{
isNativeFarmToken[_token] = _isNativeFarm;
emit SetTokenIsNativeFarm(_token, _isNativeFarm);
}
/// @dev Set the maximum bonus BP for native farms
function setMaxBonusBP(uint256 _maxBonusBP)
public
onlyOwner
{
require(_maxBonusBP <= 1000, "Max bonus is 10%");
maxBonusBP = _maxBonusBP;
emit SetMaxBonusBP(_maxBonusBP);
}
}
//SPDX-License-Identifier: MIT
pragma solidity 0.8.2;
import "./interfaces/ISummitRNGModule.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
/*
---------------------------------------------------------------------------------------------
-- S U M M I T . D E F I
---------------------------------------------------------------------------------------------
Summit is highly experimental.
It has been crafted to bring a new flavor to the defi world.
We hope you enjoy the Summit.defi experience.
If you find any bugs in these contracts, please claim the bounty (see docs)
Created with love by Architect and the Summit team
---------------------------------------------------------------------------------------------
-- E L E V A T I O N H E L P E R
---------------------------------------------------------------------------------------------
ElevationHelper.sol handles shared functionality between the elevations / expedition
Handles the allocation multiplier for each elevation
Handles the duration of each round
*/
contract ElevationHelper is Ownable {
// ---------------------------------------
// -- V A R I A B L E S
// ---------------------------------------
address public cartographer; // Allows cartographer to act as secondary owner-ish
address public expeditionV2;
// Constants for elevation comparisons
uint8 constant OASIS = 0;
uint8 constant PLAINS = 1;
uint8 constant MESA = 2;
uint8 constant SUMMIT = 3;
uint8 constant EXPEDITION = 4;
uint8 constant roundEndLockoutDuration = 120;
uint16[5] public allocMultiplier = [100, 110, 125, 150, 100]; // Alloc point multipliers for each elevation
uint16[5] public pendingAllocMultiplier = [100, 110, 125, 150, 100]; // Pending alloc point multipliers for each elevation, updated at end of round for elevation, instantly for oasis
uint8[5] public totemCount = [1, 2, 5, 10, 2]; // Number of totems at each elevation
uint256 constant baseRoundDuration = 3600; // Duration (seconds) of the smallest round chunk
uint256[5] public durationMult = [0, 2, 2, 2, 24]; // Number of round chunks for each elevation
uint256[5] public pendingDurationMult = [0, 2, 2, 2, 24]; // Duration mult that takes effect at the end of the round
uint256[5] public unlockTimestamp; // Time at which each elevation unlocks to the public
uint256[5] public roundNumber; // Current round of each elevation
uint256[5] public roundEndTimestamp; // Time at which each elevation's current round ends
mapping(uint256 => uint256) public expeditionDeityDivider; // Higher / lower integer for each expedition round
mapping(uint8 => mapping(uint256 => uint256)) public totemWinsAccum; // Accumulator of the total number of wins for each totem
mapping(uint8 => mapping(uint256 => uint8)) public winningTotem; // The specific winning totem for each elevation round
address public summitRNGModuleAdd; // VRF module address
// ---------------------------------------
// -- E V E N T S
// ---------------------------------------
event WinningTotemSelected(uint8 indexed elevation, uint256 indexed round, uint8 indexed totem);
event DeityDividerSelected(uint256 indexed expeditionRound, uint256 indexed deityDivider);
event UpgradeSummitRNGModule(address indexed _summitRNGModuleAdd);
event SetElevationRoundDurationMult(uint8 indexed _elevation, uint8 _mult);
event SetElevationAllocMultiplier(uint8 indexed _elevation, uint16 _allocMultiplier);
// ---------------------------------------
// -- I N I T I A L I Z A T I O N
// ---------------------------------------
/// @dev Creates ElevationHelper contract with cartographer as owner of certain functionality
/// @param _cartographer Address of main Cartographer contract
constructor(address _cartographer, address _expeditionV2) {
require(_cartographer != address(0), "Cartographer missing");
require(_expeditionV2 != address(0), "Expedition missing");
cartographer = _cartographer;
expeditionV2 = _expeditionV2;
}
/// @dev Turns on the Summit ecosystem across all contracts
/// @param _enableTimestamp Timestamp at which Summit was enabled, used to set unlock points for each elevation
function enable(uint256 _enableTimestamp)
external
onlyCartographer
{
// The next top of hour from the enable timestamp
uint256 nextHourTimestamp = _enableTimestamp + (3600 - (_enableTimestamp % 3600));
// Setting when each elevation of the ecosystem unlocks
unlockTimestamp = [
nextHourTimestamp, // Oasis - throwaway
nextHourTimestamp + 0 days, // Plains
nextHourTimestamp + 2 days, // Mesa
nextHourTimestamp + 4 days, // Summit
nextHourTimestamp + 5 days // Expedition
];
// The first 'round' ends when the elevation unlocks
roundEndTimestamp = unlockTimestamp;
// Timestamp of the first seed round starting
ISummitRNGModule(summitRNGModuleAdd).setSeedRoundEndTimestamp(unlockTimestamp[PLAINS] - roundEndLockoutDuration);
}
// ---------------------------------------
// -- M O D I F I E R S
// ---------------------------------------
modifier onlyCartographer() {
require(msg.sender == cartographer, "Only cartographer");
_;
}
modifier onlyCartographerOrExpedition() {
require(msg.sender == cartographer || msg.sender == expeditionV2, "Only cartographer or expedition");
_;
}
modifier allElevations(uint8 _elevation) {
require(_elevation <= EXPEDITION, "Bad elevation");
_;
}
modifier elevationOrExpedition(uint8 _elevation) {
require(_elevation >= PLAINS && _elevation <= EXPEDITION, "Bad elevation");
_;
}
// ---------------------------------------
// -- U T I L S (inlined for brevity)
// ---------------------------------------
/// @dev Allocation multiplier of an elevation
/// @param _elevation Desired elevation
function elevationAllocMultiplier(uint8 _elevation) public view returns (uint256) {
return uint256(allocMultiplier[_elevation]);
}
/// @dev Duration of elevation round in seconds
/// @param _elevation Desired elevation
function roundDurationSeconds(uint8 _elevation) public view returns (uint256) {
return durationMult[_elevation] * baseRoundDuration;
}
/// @dev Current round of the expedition
function currentExpeditionRound() public view returns (uint256) {
return roundNumber[EXPEDITION];
}
/// @dev Deity divider (random offset which skews chances of each deity winning) of the current expedition round
function currentDeityDivider() public view returns (uint256) {
return expeditionDeityDivider[currentExpeditionRound()];
}
/// @dev Modifies a given alloc point with the multiplier of that elevation, used to set a single allocation for a token while each elevation is set automatically
/// @param _allocPoint Base alloc point to modify
/// @param _elevation Fetcher for the elevation multiplier
function elevationModulatedAllocation(uint256 _allocPoint, uint8 _elevation) external view allElevations(_elevation) returns (uint256) {
return _allocPoint * allocMultiplier[_elevation];
}
/// @dev Checks whether elevation is is yet to be unlocked for farming
/// @param _elevation Which elevation to check
function elevationLocked(uint8 _elevation) external view returns (bool) {
return block.timestamp <= unlockTimestamp[_elevation];
}
/// @dev Checks whether elevation is locked due to round ending in next {roundEndLockoutDuration} seconds
/// @param _elevation Which elevation to check
function endOfRoundLockoutActive(uint8 _elevation) external view returns (bool) {
if (roundEndTimestamp[_elevation] == 0) return false;
return block.timestamp >= (roundEndTimestamp[_elevation] - roundEndLockoutDuration);
}
/// @dev The next round available for a new pool to unlock at. Used to add pools but not start them until the next rollover
/// @param _elevation Which elevation to check
function nextRound(uint8 _elevation) external view returns (uint256) {
return block.timestamp <= unlockTimestamp[_elevation] ? 1 : (roundNumber[_elevation] + 1);
}
/// @dev Whether a round has ended
/// @param _elevation Which elevation to check
function roundEnded(uint8 _elevation) internal view returns (bool) {
return block.timestamp >= roundEndTimestamp[_elevation];
}
/// @dev Seconds remaining in round of elevation
/// @param _elevation Which elevation to check time remaining of
function timeRemainingInRound(uint8 _elevation) public view returns (uint256) {
return roundEnded(_elevation) ? 0 : roundEndTimestamp[_elevation] - block.timestamp;
}
/// @dev Getter of fractional amount of round remaining
/// @param _elevation Which elevation to check progress of
/// @return Fraction raised to 1e12
function fractionRoundRemaining(uint8 _elevation) external view returns (uint256) {
return timeRemainingInRound(_elevation) * 1e12 / roundDurationSeconds(_elevation);
}
/// @dev Getter of fractional progress through round
/// @param _elevation Which elevation to check progress of
/// @return Fraction raised to 1e12
function fractionRoundComplete(uint8 _elevation) external view returns (uint256) {
return ((roundDurationSeconds(_elevation) - timeRemainingInRound(_elevation)) * 1e12) / roundDurationSeconds(_elevation);
}
/// @dev Start timestamp of current round
/// @param _elevation Which elevation to check
function currentRoundStartTime(uint8 _elevation) external view returns(uint256) {
return roundEndTimestamp[_elevation] - roundDurationSeconds(_elevation);
}
// ------------------------------------------------------------------
// -- P A R A M E T E R S
// ------------------------------------------------------------------
/// @dev Upgrade the RNG module when VRF becomes available on FTM, will only use `getRandomNumber` functionality
/// @param _summitRNGModuleAdd Address of new VRF randomness module
function upgradeSummitRNGModule (address _summitRNGModuleAdd)
public
onlyOwner
{
require(_summitRNGModuleAdd != address(0), "SummitRandomnessModule missing");
summitRNGModuleAdd = _summitRNGModuleAdd;
emit UpgradeSummitRNGModule(_summitRNGModuleAdd);
}
/// @dev Update round duration mult of an elevation
function setElevationRoundDurationMult(uint8 _elevation, uint8 _mult)
public
onlyOwner elevationOrExpedition(_elevation)
{
require(_mult > 0, "Duration mult must be non zero");
pendingDurationMult[_elevation] = _mult;
emit SetElevationRoundDurationMult(_elevation, _mult);
}
/// @dev Update emissions multiplier of an elevation
function setElevationAllocMultiplier(uint8 _elevation, uint16 _allocMultiplier)
public
onlyOwner allElevations(_elevation)
{
require(_allocMultiplier <= 300, "Multiplier cannot exceed 3X");
pendingAllocMultiplier[_elevation] = _allocMultiplier;
if (_elevation == OASIS) {
allocMultiplier[_elevation] = _allocMultiplier;
}
emit SetElevationAllocMultiplier(_elevation, _allocMultiplier);
}
// ------------------------------------------------------------------
// -- R O L L O V E R E L E V A T I O N R O U N D
// ------------------------------------------------------------------
/// @dev Validates that the selected elevation is able to be rolled over
/// @param _elevation Which elevation is attempting to be rolled over
function validateRolloverAvailable(uint8 _elevation)
external view
{
// Elevation must be unlocked for round to rollover
require(block.timestamp >= unlockTimestamp[_elevation], "Elevation locked");
// Rollover only becomes available after the round has ended, if timestamp is before roundEnd, the round has already been rolled over and its end timestamp pushed into the future
require(block.timestamp >= roundEndTimestamp[_elevation], "Round already rolled over");
}
/// @dev Uses the seed and future block number to generate a random number, which is then used to select the winning totem and if necessary the next deity divider
/// @param _elevation Which elevation to select winner for
function selectWinningTotem(uint8 _elevation)
external
onlyCartographerOrExpedition elevationOrExpedition(_elevation)
{
uint256 rand = ISummitRNGModule(summitRNGModuleAdd).getRandomNumber(_elevation, roundNumber[_elevation]);
// Uses the random number to select the winning totem
uint8 winner = chooseWinningTotem(_elevation, rand);
// Updates data with the winning totem
markWinningTotem(_elevation, winner);
// If necessary, uses the random number to generate the next deity divider for expeditions
if (_elevation == EXPEDITION)
setNextDeityDivider(rand);
}
/// @dev Final step in the rollover pipeline, incrementing the round numbers to bring current
/// @param _elevation Which elevation is being updated
function rolloverElevation(uint8 _elevation)
external
onlyCartographerOrExpedition
{
// Incrementing round number, does not need to be adjusted with overflown rounds
roundNumber[_elevation] += 1;
// Failsafe to cover multiple rounds needing to be rolled over if no user rolled them over previously (almost impossible, but just in case)
uint256 overflownRounds = ((block.timestamp - roundEndTimestamp[_elevation]) / roundDurationSeconds(_elevation));
// Brings current with any extra overflown rounds
roundEndTimestamp[_elevation] += roundDurationSeconds(_elevation) * overflownRounds;
// Updates round duration if necessary
if (pendingDurationMult[_elevation] != durationMult[_elevation]) {
durationMult[_elevation] = pendingDurationMult[_elevation];
}
// Adds the duration of the current round (updated if necessary) to the current round end timestamp
roundEndTimestamp[_elevation] += roundDurationSeconds(_elevation);
// Updates elevation allocation multiplier if necessary
if (pendingAllocMultiplier[_elevation] != allocMultiplier[_elevation]) {
allocMultiplier[_elevation] = pendingAllocMultiplier[_elevation];
}
}
/// @dev Simple modulo on generated random number to choose the winning totem (inlined for brevity)
/// @param _elevation Which elevation the winner will be selected for
/// @param _rand The generated random number to select with
function chooseWinningTotem(uint8 _elevation, uint256 _rand) internal view returns (uint8) {
if (_elevation == EXPEDITION)
return (_rand % 100) < currentDeityDivider() ? 0 : 1;
return uint8((_rand * totemCount[_elevation]) / 100);
}
/// @dev Stores selected winning totem (inlined for brevity)
/// @param _elevation Elevation to store at
/// @param _winner Selected winning totem
function markWinningTotem(uint8 _elevation, uint8 _winner) internal {
// No totem marked as the winner for the first round (unlocking the elevation)
if (roundNumber[_elevation] == 0) return;
totemWinsAccum[_elevation][_winner] += 1;
winningTotem[_elevation][roundNumber[_elevation]] = _winner;
emit WinningTotemSelected(_elevation, roundNumber[_elevation], _winner);
}
/// @dev Sets random deity divider (50 - 90) for next expedition round (inlined for brevity)
/// @param _rand Same rand that chose winner
function setNextDeityDivider(uint256 _rand) internal {
// Number between 50 - 90 based on previous round winning number, helps to balance the divider between the deities
uint256 expedSelector = (_rand % 100);
uint256 divider = 50 + ((expedSelector * 40) / 100);
expeditionDeityDivider[currentExpeditionRound() + 1] = divider;
emit DeityDividerSelected(currentExpeditionRound() + 1, divider);
}
// ------------------------------------------------------------------
// -- F R O N T E N D
// ------------------------------------------------------------------
/// @dev Fetcher of historical data for past winning totems
/// @param _elevation Which elevation to check historical winners of
/// @return Array of 20 values, first 10 of which are win count accumulators for each totem, last 10 of which are winners of previous 10 rounds of play
function historicalWinningTotems(uint8 _elevation) public view allElevations(_elevation) returns (uint256[] memory, uint256[] memory) {
uint256 round = roundNumber[_elevation];
uint256 winHistoryDepth = Math.min(10, round - 1);
uint256[] memory winsAccum = new uint256[](totemCount[_elevation]);
uint256[] memory prevWinHistory = new uint256[](winHistoryDepth);
if (_elevation > OASIS) {
uint256 prevRound = round == 0 ? 0 : round - 1;
for (uint8 i = 0; i < totemCount[_elevation]; i++) {
winsAccum[i] = totemWinsAccum[_elevation][i];
}
for (uint8 j = 0; j < winHistoryDepth; j++) {
prevWinHistory[j] = winningTotem[_elevation][prevRound - j];
}
}
return (winsAccum, prevWinHistory);
}
}
//SPDX-License-Identifier: MIT
pragma solidity 0.8.2;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface ISubCart {
function initialize(address _elevationHelper, address _summit) external;
function enable(uint256 _launchTimestamp) external;
function add(address _token, bool _live) external;
function set(address _token, bool _live) external;
function massUpdatePools() external;
function rollover() external;
function switchTotem(uint8 _totem, address _userAdd) external;
function claimElevation(address _userAdd) external returns (uint256);
function deposit(address _token, uint256 _amount, address _userAdd, bool _isElevate) external returns (uint256);
function emergencyWithdraw(address _token, address _userAdd) external returns (uint256);
function withdraw(address _token, uint256 _amount, address _userAdd, bool _isElevate) external returns (uint256);
function supply(address _token) external view returns (uint256);
function isTotemSelected(address _userAdd) external view returns (bool);
function userStakedAmount(address _token, address _userAdd) external view returns (uint256);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using Address for address;
function safeTransfer(
IERC20 token,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(
IERC20 token,
address from,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Deprecated. This function has issues similar to the ones found in
* {IERC20-approve}, and its usage is discouraged.
*
* Whenever possible, use {safeIncreaseAllowance} and
* {safeDecreaseAllowance} instead.
*/
function safeApprove(
IERC20 token,
address spender,
uint256 value
) internal {
// safeApprove should only be called when setting an initial allowance,
// or when resetting it to zero. To increase and decrease it, use
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
function safeIncreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
uint256 newAllowance = token.allowance(address(this), spender) + value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
function safeDecreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
uint256 newAllowance = oldAllowance - value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
// Return data is optional
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
* behind a proxy. Since a proxied contract can't have a constructor, it's common to move constructor logic to an
* external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
* function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
*
* TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
* possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
*
* CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
* that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
*/
abstract contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
*/
bool private _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;
/**
* @dev Modifier to protect an initializer function from being invoked twice.
*/
modifier initializer() {
require(_initializing || !_initialized, "Initializable: contract is already initialized");
bool isTopLevelCall = !_initializing;
if (isTopLevelCall) {
_initializing = true;
_initialized = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and make it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
// On the first call to nonReentrant, _notEntered will be true
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
_;
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Library for managing
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
* types.
*
* Sets have the following properties:
*
* - Elements are added, removed, and checked for existence in constant time
* (O(1)).
* - Elements are enumerated in O(n). No guarantees are made on the ordering.
*
* ```
* contract Example {
* // Add the library methods
* using EnumerableSet for EnumerableSet.AddressSet;
*
* // Declare a set state variable
* EnumerableSet.AddressSet private mySet;
* }
* ```
*
* As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
* and `uint256` (`UintSet`) are supported.
*/
library EnumerableSet {
// To implement this library for multiple types with as little code
// repetition as possible, we write it in terms of a generic Set type with
// bytes32 values.
// The Set implementation uses private functions, and user-facing
// implementations (such as AddressSet) are just wrappers around the
// underlying Set.
// This means that we can only create new EnumerableSets for types that fit
// in bytes32.
struct Set {
// Storage of set values
bytes32[] _values;
// Position of the value in the `values` array, plus 1 because index 0
// means a value is not in the set.
mapping(bytes32 => uint256) _indexes;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function _add(Set storage set, bytes32 value) private returns (bool) {
if (!_contains(set, value)) {
set._values.push(value);
// The value is stored at length-1, but we add 1 to all indexes
// and use 0 as a sentinel value
set._indexes[value] = set._values.length;
return true;
} else {
return false;
}
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function _remove(Set storage set, bytes32 value) private returns (bool) {
// We read and store the value's index to prevent multiple reads from the same storage slot
uint256 valueIndex = set._indexes[value];
if (valueIndex != 0) {
// Equivalent to contains(set, value)
// To delete an element from the _values array in O(1), we swap the element to delete with the last one in
// the array, and then remove the last element (sometimes called as 'swap and pop').
// This modifies the order of the array, as noted in {at}.
uint256 toDeleteIndex = valueIndex - 1;
uint256 lastIndex = set._values.length - 1;
if (lastIndex != toDeleteIndex) {
bytes32 lastvalue = set._values[lastIndex];
// Move the last value to the index where the value to delete is
set._values[toDeleteIndex] = lastvalue;
// Update the index for the moved value
set._indexes[lastvalue] = valueIndex; // Replace lastvalue's index to valueIndex
}
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the index for the deleted slot
delete set._indexes[value];
return true;
} else {
return false;
}
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function _contains(Set storage set, bytes32 value) private view returns (bool) {
return set._indexes[value] != 0;
}
/**
* @dev Returns the number of values on the set. O(1).
*/
function _length(Set storage set) private view returns (uint256) {
return set._values.length;
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function _at(Set storage set, uint256 index) private view returns (bytes32) {
return set._values[index];
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function _values(Set storage set) private view returns (bytes32[] memory) {
return set._values;
}
// Bytes32Set
struct Bytes32Set {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _add(set._inner, value);
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _remove(set._inner, value);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
return _contains(set._inner, value);
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(Bytes32Set storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
return _at(set._inner, index);
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
return _values(set._inner);
}
// AddressSet
struct AddressSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(AddressSet storage set, address value) internal returns (bool) {
return _add(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(AddressSet storage set, address value) internal returns (bool) {
return _remove(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(AddressSet storage set, address value) internal view returns (bool) {
return _contains(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(AddressSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(AddressSet storage set, uint256 index) internal view returns (address) {
return address(uint160(uint256(_at(set._inner, index))));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(AddressSet storage set) internal view returns (address[] memory) {
bytes32[] memory store = _values(set._inner);
address[] memory result;
assembly {
result := store
}
return result;
}
// UintSet
struct UintSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(UintSet storage set, uint256 value) internal returns (bool) {
return _add(set._inner, bytes32(value));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(UintSet storage set, uint256 value) internal returns (bool) {
return _remove(set._inner, bytes32(value));
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(UintSet storage set, uint256 value) internal view returns (bool) {
return _contains(set._inner, bytes32(value));
}
/**
* @dev Returns the number of values on the set. O(1).
*/
function length(UintSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(UintSet storage set, uint256 index) internal view returns (uint256) {
return uint256(_at(set._inner, index));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(UintSet storage set) internal view returns (uint256[] memory) {
bytes32[] memory store = _values(set._inner);
uint256[] memory result;
assembly {
result := store
}
return result;
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.2;
import "./libs/ERC20Mintable.sol";
import "./PresetPausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
contract SummitToken is ERC20Mintable('SummitToken', 'SUMMIT') {}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.2;
import "./Cartographer.sol";
import "./interfaces/ISubCart.sol";
import "./SummitToken.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
/*
---------------------------------------------------------------------------------------------
-- S U M M I T . D E F I
---------------------------------------------------------------------------------------------
Summit is highly experimental.
It has been crafted to bring a new flavor to the defi world.
We hope you enjoy the Summit.defi experience.
If you find any bugs in these contracts, please claim the bounty (see docs)
Created with love by Architect and the Summit team
---------------------------------------------------------------------------------------------
-- O A S I S E X P L A N A T I O N
---------------------------------------------------------------------------------------------
The OASIS is the safest of the elevations.
OASIS pools exactly mirror standard yield farming experiences of other projects.
OASIS pools guarantee yield, and no multiplying or risk takes place at this elevation.
The OASIS does not have totems in the contract, however in the frontend funds staked in the OASIS are represented by the OTTER.
*/
contract CartographerOasis is ISubCart, Initializable, ReentrancyGuard {
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.AddressSet;
// ---------------------------------------
// -- V A R I A B L E S
// ---------------------------------------
Cartographer public cartographer;
uint256 public launchTimestamp = 1672527600; // 2023-1-1, will be updated when summit ecosystem switched on
uint8 constant OASIS = 0; // Named constant to make reusable elevation functions easier to parse visually
address public summitTokenAddress;
struct UserInfo {
uint256 debt; // Debt is (accSummitPerShare * staked) at time of staking and is used in the calculation of yield.
uint256 staked; // The amount a user invests in an OASIS pool
}
mapping(address => EnumerableSet.AddressSet) userInteractingPools;
struct OasisPoolInfo {
address token; // Reward token yielded by the pool
uint256 supply; // Running total of the amount of tokens staked in the pool
bool live; // Turns on and off the pool
uint256 lastRewardTimestamp; // Latest timestamp that SUMMIT distribution occurred
uint256 accSummitPerShare; // Accumulated SUMMIT per share, raised to 1e12
}
EnumerableSet.AddressSet poolTokens;
mapping(address => OasisPoolInfo) public poolInfo; // Pool info for each oasis pool
mapping(address => mapping(address => UserInfo)) public userInfo; // Users running staking information
// ---------------------------------------
// -- A D M I N I S T R A T I O N
// ---------------------------------------
/// @dev Constructor simply setting address of the cartographer
constructor(address _Cartographer)
{
require(_Cartographer != address(0), "Cartographer required");
cartographer = Cartographer(_Cartographer);
}
/// @dev Unused initializer as part of the SubCartographer interface
function initialize(address, address _summitTokenAddress)
external override
initializer onlyCartographer
{
require(_summitTokenAddress != address(0), "SummitToken is zero");
summitTokenAddress = _summitTokenAddress;
}
/// @dev Enables the Summit ecosystem with a timestamp, called by the Cartographer
function enable(uint256 _launchTimestamp)
external override
onlyCartographer
{
launchTimestamp = _launchTimestamp;
}
// -----------------------------------------------------------------
// -- M O D I F I E R S (Many are split to save contract size)
// -----------------------------------------------------------------
modifier onlyCartographer() {
require(msg.sender == address(cartographer), "Only cartographer");
_;
}
modifier validUserAdd(address userAdd) {
require(userAdd != address(0), "User not 0");
_;
}
modifier nonDuplicated(address _token) {
require(!poolTokens.contains(_token), "duplicated!");
_;
}
modifier poolExists(address _token) {
require(poolTokens.contains(_token), "Pool doesnt exist");
_;
}
// ---------------------------------------
// -- U T I L S (inlined for brevity)
// ---------------------------------------
function supply(address _token) external view override returns (uint256) {
return poolInfo[_token].supply;
}
function isTotemSelected(address) external pure override returns (bool) {
return true;
}
function userStakedAmount(address _token, address _userAdd) external view override returns (uint256) {
return userInfo[_token][_userAdd].staked;
}
function getUserInteractingPools(address _userAdd) public view returns (address[] memory) {
return userInteractingPools[_userAdd].values();
}
function getPools() public view returns (address[] memory) {
return poolTokens.values();
}
// ---------------------------------------
// -- P O O L M A N A G E M E N T
// ---------------------------------------
/// @dev Creates a pool at the oasis
/// @param _token Pool token
/// @param _live Whether the pool is enabled initially
function add(address _token, bool _live)
external override
onlyCartographer nonDuplicated(_token)
{
// Add token to poolTokens
poolTokens.add(_token);
// Create the initial state of the pool
poolInfo[_token] = OasisPoolInfo({
token: _token,
supply: 0,
live: _live,
accSummitPerShare: 0,
lastRewardTimestamp: block.timestamp
});
}
/// @dev Update a given pools deposit or live status
/// @param _token Pool token identifier
/// @param _live If pool is available for staking
function set(address _token, bool _live)
external override
onlyCartographer poolExists(_token)
{
OasisPoolInfo storage pool = poolInfo[_token];
updatePool(_token);
// Update internal pool states
pool.live = _live;
// Update IsEarning in Cartographer
_updateTokenIsEarning(pool);
}
/// @dev Mark whether this token is earning at this elevation in the Cartographer
/// Live must be true
/// Launched must be true
/// Staked supply must be non zero
function _updateTokenIsEarning(OasisPoolInfo storage pool)
internal
{
cartographer.setIsTokenEarningAtElevation(
pool.token,
OASIS,
pool.live && pool.supply > 0
);
}
/// @dev Update all pools to current timestamp before other pool management transactions
function massUpdatePools()
external override
onlyCartographer
{
for (uint16 index = 0; index < poolTokens.length(); index++) {
updatePool(poolTokens.at(index));
}
}
/// @dev Bring reward variables of given pool current
/// @param _token Pool identifier to update
function updatePool(address _token)
public
poolExists(_token)
{
OasisPoolInfo storage pool = poolInfo[_token];
// Early exit if pool already current
if (pool.lastRewardTimestamp == block.timestamp) { return; }
// Early exit if pool not launched, has 0 supply, or isn't live.
// Still update last rewarded timestamp to prevent over emitting on first block on return to live
if (block.timestamp < launchTimestamp || pool.supply == 0 || !pool.live) {
pool.lastRewardTimestamp = block.timestamp;
return;
}
// Ensure that pool doesn't earn rewards from before summit ecosystem launched
if (pool.lastRewardTimestamp < launchTimestamp) {
pool.lastRewardTimestamp = launchTimestamp;
return;
}
// Mint Summit according to pool allocation and token share in pool, retrieve amount of summit minted for staking
uint256 summitReward = cartographer.poolSummitEmission(pool.lastRewardTimestamp, pool.token, OASIS);
// Update accSummitPerShare with the amount of staking summit minted.
pool.accSummitPerShare = pool.accSummitPerShare + (summitReward * 1e12 / pool.supply);
// Bring last reward timestamp current
pool.lastRewardTimestamp = block.timestamp;
}
// ---------------------------------------
// -- P O O L R E W A R D S
// ---------------------------------------
/// @dev Claimable rewards of a pool
function _poolClaimableRewards(OasisPoolInfo storage pool, UserInfo storage user)
internal view
poolExists(pool.token)
returns (uint256)
{
// Temporary accSummitPerShare to bring rewards current if last reward timestamp is behind current timestamp
uint256 accSummitPerShare = pool.accSummitPerShare;
// Bring current if last reward timestamp is in past
if (block.timestamp > launchTimestamp && block.timestamp > pool.lastRewardTimestamp && pool.supply != 0 && pool.live) {
// Fetch the pool's summit yield emission to bring current
uint256 poolSummitEmission = cartographer.poolSummitEmission(
pool.lastRewardTimestamp < launchTimestamp ? launchTimestamp : pool.lastRewardTimestamp,
pool.token,
OASIS);
// Recalculate accSummitPerShare with additional yield emission included
accSummitPerShare = accSummitPerShare + (poolSummitEmission * 1e12 / pool.supply);
}
return (user.staked * accSummitPerShare / 1e12) - user.debt;
}
/// @dev Fetch guaranteed yield rewards of the pool
/// @param _token Pool to fetch rewards from
/// @param _userAdd User requesting rewards info
/// @return claimableRewards: Amount of Summit available to Claim
function poolClaimableRewards(address _token, address _userAdd)
public view
poolExists(_token) validUserAdd(_userAdd)
returns (uint256)
{
return _poolClaimableRewards(
poolInfo[_token],
userInfo[_token][_userAdd]
);
}
/// @dev Claimable rewards across an entire elevation
/// @param _userAdd User Claiming
function elevClaimableRewards(address _userAdd)
public view
validUserAdd(_userAdd)
returns (uint256)
{
// Claim rewards of users active pools
uint256 claimable = 0;
// Iterate through pools the user is interacting, get claimable amount, update pool
address[] memory interactingPools = userInteractingPools[_userAdd].values();
for (uint8 index = 0; index < interactingPools.length; index++) {
// Claim winnings
claimable += _poolClaimableRewards(
poolInfo[interactingPools[index]],
userInfo[interactingPools[index]][_userAdd]
);
}
return claimable;
}
// ------------------------------------------------------------------
// -- Y I E L D G A M B L I N G S T U B S
// ------------------------------------------------------------------
function rollover() external override {}
function switchTotem(uint8, address) external override {}
// -----------------------------------------------------
// -- P O O L I N T E R A C T I O N S
// -----------------------------------------------------
/// @dev Increments or decrements user's pools at elevation staked, and adds to / removes from users list of staked pools
function _markUserInteractingWithPool(address _token, address _userAdd, bool _interacting) internal {
// Early escape if interacting state already up to date
if (userInteractingPools[_userAdd].contains(_token) == _interacting) return;
// Validate staked pool cap
require(!_interacting || userInteractingPools[_userAdd].length() < 12, "Staked pool cap (12) reached");
if (_interacting) {
userInteractingPools[_userAdd].add(_token);
} else {
userInteractingPools[_userAdd].remove(_token);
}
}
/// @dev Claim an entire elevation
/// @param _userAdd User Claiming
function claimElevation(address _userAdd)
external override
validUserAdd(_userAdd) onlyCartographer
returns (uint256)
{
// Claim rewards of users active pools
uint256 claimable = 0;
// Iterate through pools the user is interacting, get claimable amount, update pool
address[] memory interactingPools = userInteractingPools[_userAdd].values();
for (uint8 index = 0; index < interactingPools.length; index++) {
// Claim winnings
claimable += _unifiedClaim(
poolInfo[interactingPools[index]],
userInfo[interactingPools[index]][_userAdd],
_userAdd
);
}
return claimable;
}
/// @dev Stake funds in an OASIS pool
/// @param _token Pool to stake in
/// @param _amount Amount to stake
/// @param _userAdd User wanting to stake
/// @param _isElevate Whether this is the deposit half of an elevate tx
/// @return Amount deposited after deposit fee taken
function deposit(address _token, uint256 _amount, address _userAdd, bool _isElevate)
external override
nonReentrant onlyCartographer poolExists(_token) validUserAdd(_userAdd)
returns (uint256)
{
// Claim earnings from pool
_unifiedClaim(
poolInfo[_token],
userInfo[_token][_userAdd],
_userAdd
);
// Deposit amount into pool
return _unifiedDeposit(
poolInfo[_token],
userInfo[_token][_userAdd],
_amount,
_userAdd,
_isElevate
);
}
/// @dev Emergency withdraw without rewards
/// @param _token Pool to emergency withdraw from
/// @param _userAdd User emergency withdrawing
/// @return Amount emergency withdrawn
function emergencyWithdraw(address _token, address _userAdd)
external override
nonReentrant onlyCartographer poolExists(_token) validUserAdd(_userAdd)
returns (uint256)
{
OasisPoolInfo storage pool = poolInfo[_token];
UserInfo storage user = userInfo[_token][_userAdd];
// Signal cartographer to perform withdrawal function
uint256 amountAfterFee = cartographer.withdrawalTokenManagement(_userAdd, _token, user.staked);
// Update pool running supply total with amount withdrawn
pool.supply -= user.staked;
// Reset user's staked and debt
user.staked = 0;
user.debt = 0;
// If the user is interacting with this pool after the meat of the transaction completes
_markUserInteractingWithPool(_token, _userAdd, false);
// Return amount withdrawn
return amountAfterFee;
}
/// @dev Withdraw staked funds from pool
/// @param _token Pool to withdraw from
/// @param _amount Amount to withdraw
/// @param _userAdd User withdrawing
/// @param _isElevate Whether this is the withdraw half of an elevate tx
/// @return True amount withdrawn
function withdraw(address _token, uint256 _amount, address _userAdd, bool _isElevate)
external override
nonReentrant onlyCartographer poolExists(_token) validUserAdd(_userAdd)
returns (uint256)
{
_unifiedClaim(
poolInfo[_token],
userInfo[_token][_userAdd],
_userAdd
);
// Withdraw amount from pool
return _unifiedWithdraw(
poolInfo[_token],
userInfo[_token][_userAdd],
_amount,
_userAdd,
_isElevate
);
}
/// @dev Shared Claim functionality with cross compounding built in
/// @param pool OasisPoolInfo of pool to withdraw from
/// @param user UserInfo of withdrawing user
/// @param _userAdd User address
/// @return Amount claimable
function _unifiedClaim(OasisPoolInfo storage pool, UserInfo storage user, address _userAdd)
internal
returns (uint256)
{
updatePool(pool.token);
// Check claimable rewards and withdraw if applicable
uint256 claimable = (user.staked * pool.accSummitPerShare / 1e12) - user.debt;
// Claim rewards, replace claimable with true claimed amount with bonuses included
if (claimable > 0) {
claimable = cartographer.claimWinnings(_userAdd, pool.token, claimable);
}
// Set debt, may be overwritten in subsequent deposit / withdraw, but may not so it needs to be set here
user.debt = user.staked * pool.accSummitPerShare / 1e12;
// Return amount Claimed / claimable
return claimable;
}
/// @dev Internal shared deposit functionality for elevate or standard deposit
/// @param pool OasisPoolInfo of pool to deposit into
/// @param user UserInfo of depositing user
/// @param _amount Amount to deposit
/// @param _userAdd User address
/// @param _isInternalTransfer Flag to switch off certain functionality if transfer is exclusively within summit ecosystem
/// @return Amount deposited after fee taken
function _unifiedDeposit(OasisPoolInfo storage pool, UserInfo storage user, uint256 _amount, address _userAdd, bool _isInternalTransfer)
internal
returns (uint256)
{
updatePool(pool.token);
uint256 amountAfterFee = _amount;
// Handle taking fees and adding to running supply if amount depositing is non zero
if (_amount > 0) {
// Only move tokens (and take fee) on external transactions
if (!_isInternalTransfer) {
amountAfterFee = cartographer.depositTokenManagement(_userAdd, pool.token, _amount);
}
// Increment running pool supply with amount after fee taken
pool.supply += amountAfterFee;
// Update IsEarning in Cartographer
_updateTokenIsEarning(pool);
}
// Update user info with new staked value, and calculate new debt
user.staked += amountAfterFee;
user.debt = user.staked * pool.accSummitPerShare / 1e12;
// If the user is interacting with this pool after the meat of the transaction completes
_markUserInteractingWithPool(pool.token, _userAdd, user.staked > 0);
// Return amount staked after fee
return amountAfterFee;
}
/// @dev Withdraw functionality shared between standardWithdraw and elevateWithdraw
/// @param pool OasisPoolInfo of pool to withdraw from
/// @param user UserInfo of withdrawing user
/// @param _amount Amount to withdraw
/// @param _userAdd User address
/// @param _isInternalTransfer Flag to switch off certain functionality for elevate withdraw
/// @return Amount withdrawn
function _unifiedWithdraw(OasisPoolInfo storage pool, UserInfo storage user, uint256 _amount, address _userAdd, bool _isInternalTransfer)
internal
returns (uint256)
{
// Validate amount attempting to withdraw
require(_amount > 0 && user.staked >= _amount, "Bad withdrawal");
updatePool(pool.token);
// Signal cartographer to perform withdrawal function if not elevating funds
// Elevated funds remain in the cartographer, or in the passthrough target, so no need to withdraw from anywhere as they would be immediately re-deposited
uint256 amountAfterFee = _amount;
if (!_isInternalTransfer) {
amountAfterFee = cartographer.withdrawalTokenManagement(_userAdd, pool.token, _amount);
}
// Update pool running supply total with amount withdrawn
pool.supply -= _amount;
// Update IsEarning in Cartographer
_updateTokenIsEarning(pool);
// Update user's staked and debt
user.staked -= _amount;
user.debt = user.staked * pool.accSummitPerShare / 1e12;
// If the user is interacting with this pool after the meat of the transaction completes
_markUserInteractingWithPool(pool.token, _userAdd, user.staked > 0);
// Return amount withdrawn
return amountAfterFee;
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.2;
import "./BaseEverestExtension.sol";
import "./PresetPausable.sol";
import "./libs/SummitMath.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
// EverestToken, governance token of Summit DeFi
contract EverestToken is ERC20('EverestToken', 'EVEREST'), Ownable, ReentrancyGuard, PresetPausable {
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.AddressSet;
// ---------------------------------------
// -- V A R I A B L E S
// ---------------------------------------
IERC20 public summit;
bool public panic = false;
uint256 public constant daySeconds = 24 * 3600;
uint256 public minLockTime = 7 days;
uint256 public inflectionLockTime = 30 days;
uint256 public maxLockTime = 365 days;
uint256 public minEverestLockMult = 1000;
uint256 public inflectionEverestLockMult = 10000;
uint256 public maxEverestLockMult = 25000;
uint256 public totalSummitLocked;
uint256 public weightedAvgSummitLockDurations;
EnumerableSet.AddressSet whitelistedTransferAddresses;
struct UserEverestInfo {
address userAdd;
uint256 everestOwned;
uint256 everestLockMultiplier;
uint256 lockDuration;
uint256 lockRelease;
uint256 summitLocked;
}
mapping(address => UserEverestInfo) public userEverestInfo;
// Other contracts that hook into the user's amount of everest, max 3 extensions
// Will be used for the DAO, as well as everest pools in the future
EnumerableSet.AddressSet everestExtensions;
constructor(address _summit) {
require(_summit != address(0), "SummitToken missing");
summit = IERC20(_summit);
// Add burn / mintFrom address as whitelisted address
whitelistedTransferAddresses.add(address(0));
// Add this address as a whitelisted address
whitelistedTransferAddresses.add(address(this));
}
// ---------------------------------------
// -- E V E N T S
// ---------------------------------------
event SummitLocked(address indexed user, uint256 _summitLocked, uint256 _lockDuration, uint256 _everestAwarded);
event LockDurationIncreased(address indexed user, uint256 _lockDuration, uint256 _additionalEverestAwarded);
event LockedSummitIncreased(address indexed user, bool indexed _increasedWithClaimableWinnings, uint256 _summitLocked, uint256 _everestAwarded);
event LockedSummitWithdrawn(address indexed user, uint256 _summitRemoved, uint256 _everestBurned);
event PanicFundsRecovered(address indexed user, uint256 _summitRecovered);
event SetMinLockTime(uint256 _lockTimeDays);
event SetInflectionLockTime(uint256 _lockTimeDays);
event SetMaxLockTime(uint256 _lockTimeDays);
event SetMinEverestLockMult(uint256 _lockMult);
event SetInflectionEverestLockMult(uint256 _lockMult);
event SetMaxEverestLockMult(uint256 _lockMult);
event SetLockTimeRequiredForTaxlessSummitWithdraw(uint256 _lockTimeDays);
event SetLockTimeRequiredForLockedSummitDeposit(uint256 _lockTimeDays);
event AddWhitelistedTransferAddress(address _transferAddress);
event SetPanic(bool _panic);
event EverestExtensionAdded(address indexed extension);
event EverestExtensionRemoved(address indexed extension);
// ------------------------------------------------------
// -- M O D I F I E R S
// ------------------------------------------------------
modifier validLockDuration(uint256 _lockDuration) {
require (_lockDuration >= minLockTime && _lockDuration <= maxLockTime, "Invalid lock duration");
_;
}
modifier userNotAlreadyLockingSummit() {
require (userEverestInfo[msg.sender].everestOwned == 0, "Already locking summit");
_;
}
modifier userLockDurationSatisfied() {
require(userEverestInfo[msg.sender].lockRelease != 0, "User doesnt have a lock release");
require(block.timestamp >= userEverestInfo[msg.sender].lockRelease, "Lock still in effect");
_;
}
modifier userEverestInfoExists(address _userAdd) {
require(userEverestInfo[_userAdd].userAdd == _userAdd, "User doesnt exist");
_;
}
modifier userOwnsEverest(address _userAdd) {
require (userEverestInfo[_userAdd].everestOwned > 0, "Must own everest");
_;
}
modifier validEverestAmountToBurn(uint256 _everestAmount) {
require (_everestAmount > 0 && _everestAmount <= userEverestInfo[msg.sender].everestOwned, "Bad withdraw");
_;
}
modifier onlyPanic() {
require(panic, "Not in panic");
_;
}
modifier notPanic() {
require(!panic, "Not available during panic");
_;
}
// ---------------------------------------
// -- A D J U S T M E N T S
// ---------------------------------------
function setMinLockTime(uint256 _lockTimeDays) public onlyOwner {
require(_lockTimeDays <= inflectionLockTime && _lockTimeDays >= 1 && _lockTimeDays <= 30, "Invalid minimum lock time (1-30 days)");
minLockTime = _lockTimeDays * daySeconds;
emit SetMinLockTime(_lockTimeDays);
}
function setInflectionLockTime(uint256 _lockTimeDays) public onlyOwner {
require(_lockTimeDays >= minLockTime && _lockTimeDays <= maxLockTime && _lockTimeDays >= 7 && _lockTimeDays <= 365, "Invalid inflection lock time (7-365 days)");
inflectionLockTime = _lockTimeDays * daySeconds;
emit SetInflectionLockTime(_lockTimeDays);
}
function setMaxLockTime(uint256 _lockTimeDays) public onlyOwner {
require(_lockTimeDays >= inflectionLockTime && _lockTimeDays >= 7 && _lockTimeDays <= 730, "Invalid maximum lock time (7-730 days)");
maxLockTime = _lockTimeDays * daySeconds;
emit SetMaxLockTime(_lockTimeDays);
}
function setMinEverestLockMult(uint256 _lockMult) public onlyOwner {
require(_lockMult >= 100 && _lockMult <= 50000, "Invalid lock mult");
minEverestLockMult = _lockMult;
emit SetMinEverestLockMult(_lockMult);
}
function setInflectionEverestLockMult(uint256 _lockMult) public onlyOwner {
require(_lockMult >= 100 && _lockMult <= 50000, "Invalid lock mult");
inflectionEverestLockMult = _lockMult;
emit SetInflectionEverestLockMult(_lockMult);
}
function setMaxEverestLockMult(uint256 _lockMult) public onlyOwner {
require(_lockMult >= 100 && _lockMult <= 50000, "Invalid lock mult");
maxEverestLockMult = _lockMult;
emit SetMaxEverestLockMult(_lockMult);
}
// ------------------------------------------------------------
// -- F U N C T I O N A L I T Y
// ------------------------------------------------------------
/// @dev Update the average lock duration
function _updateAvgSummitLockDuration(uint256 _amount, uint256 _lockDuration, bool _isLocking)
internal
{
// The weighted average of the change being applied
uint256 deltaWeightedAvg = _amount * _lockDuration;
// Update the lock multiplier and the total amount locked
if (_isLocking) {
totalSummitLocked += _amount;
weightedAvgSummitLockDurations += deltaWeightedAvg;
} else {
totalSummitLocked -= _amount;
weightedAvgSummitLockDurations -= deltaWeightedAvg;
}
}
function avgSummitLockDuration()
public view
returns (uint256)
{
// Early escape if div/0
if (totalSummitLocked == 0) return 0;
// Return the average from the weighted average lock duration
return weightedAvgSummitLockDurations / totalSummitLocked;
}
/// @dev Lock period multiplier
function _lockDurationMultiplier(uint256 _lockDuration)
internal view
returns (uint256)
{
if (_lockDuration <= inflectionLockTime) {
return SummitMath.scaledValue(
_lockDuration,
minLockTime, inflectionLockTime,
minEverestLockMult, inflectionEverestLockMult
);
}
return SummitMath.scaledValue(
_lockDuration,
inflectionLockTime, maxLockTime,
inflectionEverestLockMult, maxEverestLockMult
);
}
/// @dev Transfer everest to the burn address.
function _burnEverest(address _userAdd, uint256 _everestAmount)
internal
{
IERC20(address(this)).safeTransferFrom(_userAdd, address(this), _everestAmount);
_burn(address(this), _everestAmount);
}
/// @dev Lock Summit for a duration and earn everest
/// @param _summitAmount Amount of SUMMIT to deposit
/// @param _lockDuration Duration the SUMMIT will be locked for
function lockSummit(uint256 _summitAmount, uint256 _lockDuration)
public whenNotPaused
nonReentrant notPanic userNotAlreadyLockingSummit validLockDuration(_lockDuration)
{
// Validate and deposit user's SUMMIT
require(_summitAmount <= summit.balanceOf(msg.sender), "Exceeds balance");
if (_summitAmount > 0) {
summit.safeTransferFrom(msg.sender, address(this), _summitAmount);
}
// Calculate the lock multiplier and EVEREST award
uint256 everestLockMultiplier = _lockDurationMultiplier(_lockDuration);
uint256 everestAward = (_summitAmount * everestLockMultiplier) / 10000;
// Mint EVEREST to the user's wallet
_mint(msg.sender, everestAward);
// Create and initialize the user's everestInfo
UserEverestInfo storage everestInfo = userEverestInfo[msg.sender];
everestInfo.userAdd = msg.sender;
everestInfo.everestOwned = everestAward;
everestInfo.everestLockMultiplier = everestLockMultiplier;
everestInfo.lockRelease = block.timestamp + _lockDuration;
everestInfo.lockDuration = _lockDuration;
everestInfo.summitLocked = _summitAmount;
// Update average lock duration with new summit locked
_updateAvgSummitLockDuration(_summitAmount, _lockDuration, true);
// Update the EVEREST in the expedition
_updateEverestExtensionsUserEverestOwned(everestInfo);
emit SummitLocked(msg.sender, _summitAmount, _lockDuration, everestAward);
}
/// @dev Increase the lock duration of user's locked SUMMIT
function increaseLockDuration(uint256 _lockDuration)
public whenNotPaused
nonReentrant notPanic userEverestInfoExists(msg.sender) userOwnsEverest(msg.sender) validLockDuration(_lockDuration)
{
uint256 additionalEverestAward = _increaseLockDuration(_lockDuration, msg.sender);
emit LockDurationIncreased(msg.sender, _lockDuration, additionalEverestAward);
}
function _increaseLockDuration(uint256 _lockDuration, address _userAdd)
internal
returns (uint256)
{
UserEverestInfo storage everestInfo = userEverestInfo[_userAdd];
require(_lockDuration > everestInfo.lockDuration, "Lock duration must strictly increase");
// Update average lock duration by removing existing lock duration, and adding new duration
_updateAvgSummitLockDuration(everestInfo.summitLocked, everestInfo.lockDuration, false);
_updateAvgSummitLockDuration(everestInfo.summitLocked, _lockDuration, true);
// Calculate and validate the new everest lock multiplier
uint256 everestLockMultiplier = _lockDurationMultiplier(_lockDuration);
require(everestLockMultiplier >= everestInfo.everestLockMultiplier, "New lock duration must be greater");
// Calculate the additional EVEREST awarded by the extended lock duration
uint256 additionalEverestAward = ((everestInfo.summitLocked * everestLockMultiplier) / 10000) - everestInfo.everestOwned;
// Increase the lock release
uint256 lockRelease = block.timestamp + _lockDuration;
// Mint EVEREST to the user's address
_mint(_userAdd, additionalEverestAward);
// Update the user's running state
everestInfo.everestOwned += additionalEverestAward;
everestInfo.everestLockMultiplier = everestLockMultiplier;
everestInfo.lockRelease = lockRelease;
everestInfo.lockDuration = _lockDuration;
// Update the expedition with the user's new EVEREST amount
_updateEverestExtensionsUserEverestOwned(everestInfo);
return additionalEverestAward;
}
/// @dev Internal locked SUMMIT amount increase, returns the extra EVEREST earned by the increased lock duration
function _increaseLockedSummit(uint256 _summitAmount, UserEverestInfo storage everestInfo, address _summitOriginAdd)
internal
returns (uint256)
{
// Validate and deposit user's funds
require(_summitAmount <= summit.balanceOf(_summitOriginAdd), "Exceeds balance");
if (_summitAmount > 0) {
summit.safeTransferFrom(_summitOriginAdd, address(this), _summitAmount);
}
// Calculate the extra EVEREST that is awarded by the deposited SUMMIT
uint256 additionalEverestAward = (_summitAmount * everestInfo.everestLockMultiplier) / 10000;
// Mint EVEREST to the user's address
_mint(everestInfo.userAdd, additionalEverestAward);
// Increase running balances of EVEREST and SUMMIT
everestInfo.everestOwned += additionalEverestAward;
everestInfo.summitLocked += _summitAmount;
// Update average lock duration with new summit locked
_updateAvgSummitLockDuration(_summitAmount, everestInfo.lockDuration, true);
// Update the expedition with the users new EVEREST info
_updateEverestExtensionsUserEverestOwned(everestInfo);
return additionalEverestAward;
}
/// @dev Increase the duration of already locked SUMMIT, exit early if user is already locked for a longer duration
/// @return Amount of additional everest earned by this increase of lock duration
function _increaseLockDurationAndReleaseIfNecessary(UserEverestInfo storage everestInfo, uint256 _lockDuration)
internal
returns (uint256)
{
// Early escape if lock release already satisfies requirement
if ((block.timestamp + _lockDuration) <= everestInfo.lockRelease) return 0;
// If required lock duration is satisfied, but lock release needs to be extended: Update lockRelease exclusively
if (_lockDuration <= everestInfo.lockDuration) {
everestInfo.lockRelease = block.timestamp + _lockDuration;
return 0;
}
// Lock duration increased: earn additional EVEREST and update lockDuration and lockRelease
return _increaseLockDuration(_lockDuration, everestInfo.userAdd);
}
/// @dev Lock additional summit and extend duration to arbitrary duration
function lockAndExtendLockDuration(uint256 _summitAmount, uint256 _lockDuration, address _userAdd)
public whenNotPaused
nonReentrant notPanic userEverestInfoExists(_userAdd) userOwnsEverest(_userAdd) validLockDuration(_lockDuration)
{
UserEverestInfo storage everestInfo = userEverestInfo[_userAdd];
// Increase the lock duration of the current locked SUMMIT
uint256 additionalEverestAward = _increaseLockDurationAndReleaseIfNecessary(everestInfo, _lockDuration);
// Increase the amount of locked summit by {_summitAmount} and increase the EVEREST award
additionalEverestAward += _increaseLockedSummit(
_summitAmount,
everestInfo,
msg.sender
);
emit LockedSummitIncreased(_userAdd, true, _summitAmount, additionalEverestAward);
}
/// @dev Increase the users Locked Summit and earn everest
function increaseLockedSummit(uint256 _summitAmount)
public whenNotPaused
nonReentrant notPanic userEverestInfoExists(msg.sender) userOwnsEverest(msg.sender)
{
uint256 additionalEverestAward = _increaseLockedSummit(
_summitAmount,
userEverestInfo[msg.sender],
msg.sender
);
emit LockedSummitIncreased(msg.sender, false, _summitAmount, additionalEverestAward);
}
/// @dev Decrease the Summit and burn everest
function withdrawLockedSummit(uint256 _everestAmount)
public whenNotPaused
nonReentrant notPanic userEverestInfoExists(msg.sender) userOwnsEverest(msg.sender) userLockDurationSatisfied validEverestAmountToBurn(_everestAmount)
{
UserEverestInfo storage everestInfo = userEverestInfo[msg.sender];
require (_everestAmount <= everestInfo.everestOwned, "Bad withdraw");
uint256 summitToWithdraw = _everestAmount * 10000 / everestInfo.everestLockMultiplier;
everestInfo.everestOwned -= _everestAmount;
everestInfo.summitLocked -= summitToWithdraw;
// Update average summit lock duration with removed summit
_updateAvgSummitLockDuration(summitToWithdraw, everestInfo.lockDuration, false);
summit.safeTransfer(msg.sender, summitToWithdraw);
_burnEverest(msg.sender, _everestAmount);
_updateEverestExtensionsUserEverestOwned(everestInfo);
emit LockedSummitWithdrawn(msg.sender, summitToWithdraw, _everestAmount);
}
// ----------------------------------------------------------------------
// -- W H I T E L I S T E D T R A N S F E R
// ----------------------------------------------------------------------
function addWhitelistedTransferAddress(address _whitelistedAddress) public onlyOwner {
require(_whitelistedAddress != address(0), "Whitelisted Address missing");
whitelistedTransferAddresses.add(_whitelistedAddress);
emit AddWhitelistedTransferAddress(_whitelistedAddress);
}
function getWhitelistedTransferAddresses() public view returns (address[] memory) {
return whitelistedTransferAddresses.values();
}
function _beforeTokenTransfer(address sender, address recipient, uint256) internal view override {
require(whitelistedTransferAddresses.contains(sender) || whitelistedTransferAddresses.contains(recipient), "Not a whitelisted transfer");
}
// ----------------------------------------------------------------------
// -- E V E R E S T E X T E N S I O N S
// ----------------------------------------------------------------------
/// @dev Add an everest extension
function addEverestExtension(address _extension)
public
onlyOwner
{
require(_extension != address(0), "Missing extension");
require(everestExtensions.length() < 5, "Max extension cap reached");
require(!everestExtensions.contains(_extension), "Extension already exists");
everestExtensions.add(_extension);
emit EverestExtensionAdded(_extension);
}
/// @dev Remove an everest extension
function removeEverestExtension(address _extension)
public
onlyOwner
{
require(_extension != address(0), "Missing extension");
require(everestExtensions.contains(_extension), "Extension doesnt exist");
everestExtensions.remove(_extension);
emit EverestExtensionRemoved(_extension);
}
/// @dev Return list of everest extensions
function getEverestExtensions()
public view
returns (address[] memory)
{
return everestExtensions.values();
}
/// @dev Get user everest owned
function getUserEverestOwned(address _userAdd)
public view
returns (uint256)
{
return userEverestInfo[_userAdd].everestOwned;
}
function _updateEverestExtensionsUserEverestOwned(UserEverestInfo storage user)
internal
{
// Iterate through and update each extension with the user's everest amount
for (uint8 extensionIndex = 0; extensionIndex < e