diff --git a/contracts/RelaySugar.vy b/contracts/RelaySugar.vy new file mode 100644 index 0000000..18267b6 --- /dev/null +++ b/contracts/RelaySugar.vy @@ -0,0 +1,193 @@ +# SPDX-License-Identifier: BUSL-1.1 +# @version >=0.3.6 <0.4.0 + +# @title Velodrome Finance Relay Sugar v2 +# @author stas, ZoomerAnon +# @notice Makes it nicer to work with Relay. + +MAX_RELAYS: constant(uint256) = 50 +MAX_RESULTS: constant(uint256) = 1000 +MAX_PAIRS: constant(uint256) = 30 +WEEK: constant(uint256) = 7 * 24 * 60 * 60 + +struct LpVotes: + lp: address + weight: uint256 + +struct Relay: + venft_id: uint256 + decimals: uint8 + amount: uint128 + voting_amount: uint256 + voted_at: uint256 + votes: DynArray[LpVotes, MAX_PAIRS] + token: address + compounded: uint256 + run_at: uint256 + manager: address + relay: address + inactive: bool + name: String[100] + account_venft_ids: DynArray[uint256, MAX_RESULTS] + +interface IVoter: + def ve() -> address: view + def lastVoted(_venft_id: uint256) -> uint256: view + def poolVote(_venft_id: uint256, _index: uint256) -> address: view + def votes(_venft_id: uint256, _lp: address) -> uint256: view + def usedWeights(_venft_id: uint256) -> uint256: view + +interface IVotingEscrow: + def idToManaged(_venft_id: uint256) -> uint256: view + def deactivated(_venft_id: uint256) -> bool: view + def token() -> address: view + def decimals() -> uint8: view + def ownerOf(_venft_id: uint256) -> address: view + def balanceOfNFT(_venft_id: uint256) -> uint256: view + def locked(_venft_id: uint256) -> (uint128, uint256, bool): view + def ownerToNFTokenIdList(_account: address, _index: uint256) -> uint256: view + def voted(_venft_id: uint256) -> bool: view + +interface IRelayRegistry: + def getAll() -> DynArray[address, MAX_RELAYS]: view + +interface IRelayFactory: + def relays() -> DynArray[address, MAX_RELAYS]: view + +interface IRelay: + def name() -> String[100]: view + def mTokenId() -> uint256: view + def token() -> address: view + def keeperLastRun() -> uint256: view + # Latest epoch rewards + def amountTokenEarned(_epoch_ts: uint256) -> uint256: view + +# Vars +registry: public(IRelayRegistry) +voter: public(IVoter) +ve: public(IVotingEscrow) +token: public(address) + +@external +def __init__(_registry: address, _voter: address): + """ + @dev Set up our external registry and voter contracts + """ + self.registry = IRelayRegistry(_registry) + self.voter = IVoter(_voter) + self.ve = IVotingEscrow(self.voter.ve()) + self.token = self.ve.token() + +@external +@view +def all(_account: address) -> DynArray[Relay, MAX_RELAYS]: + """ + @notice Returns all Relays and account's deposits + @return Array of Relay structs + """ + return self._relays(_account) + +@internal +@view +def _relays(_account: address) -> DynArray[Relay, MAX_RELAYS]: + """ + @notice Returns all Relays and account's deposits + @return Array of Relay structs + """ + relays: DynArray[Relay, MAX_RELAYS] = empty(DynArray[Relay, MAX_RELAYS]) + factories: DynArray[address, MAX_RELAYS] = self.registry.getAll() + + for factory_index in range(0, MAX_RELAYS): + if factory_index == len(factories): + break + + relay_factory: IRelayFactory = IRelayFactory(factories[factory_index]) + addresses: DynArray[address, MAX_RELAYS] = relay_factory.relays() + + for index in range(0, MAX_RELAYS): + if index == len(addresses): + break + + relay: Relay = self._byAddress(addresses[index], _account) + relays.append(relay) + + return relays + +@internal +@view +def _byAddress(_relay: address, _account: address) -> Relay: + """ + @notice Returns Relay data based on address, with optional account arg + @param _relay The Relay address to lookup + @param _account The account address to lookup deposits + @return Relay struct + """ + + relay: IRelay = IRelay(_relay) + managed_id: uint256 = relay.mTokenId() + + account_venft_ids: DynArray[uint256, MAX_RESULTS] = empty(DynArray[uint256, MAX_RESULTS]) + + for venft_index in range(MAX_RESULTS): + account_venft_id: uint256 = self.ve.ownerToNFTokenIdList(_account, venft_index) + + if account_venft_id == 0: + break + + account_venft_manager_id: uint256 = self.ve.idToManaged(account_venft_id) + if account_venft_manager_id == managed_id: + account_venft_ids.append(account_venft_id) + + votes: DynArray[LpVotes, MAX_PAIRS] = [] + amount: uint128 = self.ve.locked(managed_id)[0] + last_voted: uint256 = 0 + manager: address = self.ve.ownerOf(managed_id) + inactive: bool = self.ve.deactivated(managed_id) + + epoch_start_ts: uint256 = block.timestamp / WEEK * WEEK + + # Rewards claimed this epoch + rewards_compounded: uint256 = relay.amountTokenEarned(epoch_start_ts) + + if self.ve.voted(managed_id): + last_voted = self.voter.lastVoted(managed_id) + + vote_weight: uint256 = self.voter.usedWeights(managed_id) + # Since we don't have a way to see how many pools the veNFT voted... + left_weight: uint256 = vote_weight + + for index in range(MAX_PAIRS): + if left_weight == 0: + break + + lp: address = self.voter.poolVote(managed_id, index) + + if lp == empty(address): + break + + weight: uint256 = self.voter.votes(managed_id, lp) + + votes.append(LpVotes({ + lp: lp, + weight: weight + })) + + # Remove _counted_ weight to see if there are other pool votes left... + left_weight -= weight + + return Relay({ + venft_id: managed_id, + decimals: self.ve.decimals(), + amount: amount, + voting_amount: self.ve.balanceOfNFT(managed_id), + voted_at: last_voted, + votes: votes, + token: relay.token(), + compounded: rewards_compounded, + run_at: relay.keeperLastRun(), + manager: manager, + relay: _relay, + inactive: inactive, + name: relay.name(), + account_venft_ids: account_venft_ids + }) diff --git a/env.example b/env.example index d9187d7..50399f7 100644 --- a/env.example +++ b/env.example @@ -5,3 +5,5 @@ DIST_ADDRESS=0x9D4736EC60715e71aFe72973f7885DCBC21EA99b CONVERTOR_ADDRESS=0x585Af0b397AC42dbeF7f18395426BF878634f18D LP_SUGAR_ADDRESS=0xD2B1D1B75a0f226722b3A174dAE54e6dD14af1a1 VE_SUGAR_ADDRESS=0x0eCc2593E3a6A9be3628940Fa4D928CC257B588B +RELAY_SUGAR_ADDRESS=0x7f609cf1a99318652859aED5B00C7F5F187E0077 +RELAY_REGISTRY_ADDRESS=0xBC3dc970f891ffdd3049FA3a649985CC6626d486 diff --git a/readme.md b/readme.md index 611f28e..2e4eb5a 100644 --- a/readme.md +++ b/readme.md @@ -165,6 +165,35 @@ The available methods are: * `byId(_id: uint256) -> VeNFT` - returns the `VeNFT` struct for a specific NFT id. +### Relay Data + +`RelaySugar.vy` is deployed at `0x7f609cf1a99318652859aED5B00C7F5F187E0077` + +It allows fetching Relay autocompounder/autoconverter data. +The returned data/struct of type `Relay` values represent: + + * `venft_id` - token ID of the Relay veNFT + * `decimals` - Relay veNFT token decimals + * `amount` - Relay veNFT locked amount + * `voting_amount` - Relay veNFT voting power + * `voted_at` - Relay veNFT last vote timestamp + * `votes` - Relay veNFT list of pools with vote weights casted in the form of + `LpVotes` + * `token` - token address the Relay is compounding into + * `compounded` - amount of tokens compounded into in the recent epoch + * `run_at` - timestamp of last compounding + * `manager` - Relay manager + * `relay` - Relay address + * `inactive` - Relay active/inactive status + * `name` - Relay name + * `account_venft_ids` - token IDs of the account's deposits into this Relay + +--- + +The available methods are: + + * `all(_account: address) -> Relay[]` - returns a list of all `Relay` structs. + ## Development To setup the environment, build the Docker image first: diff --git a/tests/test_relay_sugar.py b/tests/test_relay_sugar.py new file mode 100644 index 0000000..b7479e0 --- /dev/null +++ b/tests/test_relay_sugar.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: BUSL-1.1 +import os +import pytest + +from collections import namedtuple +from web3.constants import ADDRESS_ZERO + + +@pytest.fixture +def sugar_contract(RelaySugar, accounts): + # Since we depend on the rest of the protocol, + # we just point to an existing deployment + yield RelaySugar.at(os.getenv('RELAY_SUGAR_ADDRESS')) + + +@pytest.fixture +def RelayStruct(sugar_contract): + method_output = sugar_contract.all.abi['outputs'][0] + members = list(map(lambda _e: _e['name'], method_output['components'])) + + yield namedtuple('RelayStruct', members) + + +def test_initial_state(sugar_contract): + assert sugar_contract.voter() == os.getenv('VOTER_ADDRESS') + assert sugar_contract.registry() == \ + os.getenv('RELAY_REGISTRY_ADDRESS') + + +def test_all(sugar_contract, RelayStruct): + relays = list(map( + lambda _r: RelayStruct(*_r), + sugar_contract.all(ADDRESS_ZERO) + )) + + assert relays is not None