Skip to content

Commit

Permalink
Merge branch 'main' into circulating-supply
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertoCentonze committed Sep 18, 2024
2 parents 6f7b16c + d659cc7 commit f85fabc
Show file tree
Hide file tree
Showing 11 changed files with 1,423 additions and 167 deletions.
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 100
41 changes: 37 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
name: pre-commit
name: CI Workflow

on: [push]
on: [push, pull_request]

jobs:

# Linting with Pre-commit
lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup Python 3.10.4
- name: Setup Python 3.12.6
uses: actions/setup-python@v4
with:
python-version: 3.10.4
python-version: 3.12.6

# Run pre-commit hooks (e.g., black, flake8, isort)
- uses: pre-commit/[email protected]


# Test runs
tests:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false

steps:
- uses: actions/checkout@v3

# install poetry to allow cacheing
- name: Install poetry
run: pipx install poetry

- name: Setup Python 3.12.6
uses: actions/setup-python@v5
with:
python-version: 3.12.6
cache: 'poetry'
- name: Install Requirements
run: |
poetry config virtualenvs.in-project true
poetry install
- name: Run Tests
env:
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
run: |
poetry run pytest -n auto
55 changes: 53 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,53 @@
.idea
__pycache__
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Distribution / packaging
.Python
build/
dist/
*.egg-info/
.eggs/
*.egg
*.wheel
MANIFEST

# Unit test / coverage reports
.tox/
.nox/
.coverage
coverage.xml
*.cover
*.py,cover
.pytest_cache/
htmlcov/

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Jupyter Notebook
.ipynb_checkpoints/

# Pyre type checker
.pyre/

# macOS specific files
.DS_Store

# IDE files
.idea/
.vscode/

# Log files
*.log

# Temporary files
*.tmp
*.swp
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ repos:
- id: check-ast
- id: detect-private-key

- repo: https://github.com/psf/black
rev: 22.3.0
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.8.0
hooks:
- id: black
args: [--line-length=79]
args: [--line-length=100]
- repo: https://github.com/pycqa/flake8
rev: 4.0.1
rev: 7.1.1
hooks:
- id: flake8
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
args: ["--profile", "black", --line-length=79]
args: ["--profile", "black", --line-length=100]

default_language_version:
python: python3.10.4
python: python3.12.6
38 changes: 25 additions & 13 deletions contracts/RewardsHandler.vy
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,38 @@

from ethereum.ercs import IERC20
from ethereum.ercs import IERC165

from interfaces import IVault
from interfaces import IStablecoinLens as ILens

from snekmate.auth import access_control

initializes: access_control

from interfaces import IStablecoinLens as ILens

import TWAP as twap
import TWA as twa
initializes: twa
exports: (
twa.compute_twa,
twa.snapshots,
twa.get_len_snapshots,
twa.twa_window,
)

RATE_ADMIN: public(constant(bytes32)) = keccak256("RATE_ADMIN")
WEEK: constant(uint256) = 86400 * 7 # 7 days

stablecoin: immutable(address)
vault: immutable(address)
# TODO should this be mutable and have a 22k overhead or immutable for just 0.1k
lens: address
WEEK: constant(uint256) = 86400 * 7 # 7 days

# implements: IERC165
initializes: twap


@deploy
def __init__(_stablecoin: address, _vault: address):
access_control.__init__()
twa.__init__(WEEK, 1) # twa_window = 1 week, min_snapshot_dt_seconds = 1 second (if 0, then spam is possible)
stablecoin = _stablecoin
vault = _vault

Expand All @@ -36,24 +43,30 @@ def take_snapshot():
total_supply: uint256 = staticcall ILens(self.lens).circulating_supply()
supply_in_vault: uint256 = staticcall IERC20(stablecoin).balanceOf(vault)
supply_ratio: uint256 = supply_in_vault * 10**18 // total_supply
twa.store_snapshot(supply_ratio)

twap.take_snapshot(supply_ratio)

def supportsInterface(id: bytes4) -> bool:
return True


@external
@view
def tvl_twap() -> uint256:
return twap.compute()
def adjust_twa_frequency(_min_snapshot_dt_seconds: uint256):
access_control._check_role(RATE_ADMIN, msg.sender)
twa.adjust_min_snapshot_dt_seconds(_min_snapshot_dt_seconds)


def supportsInterface(id: bytes4) -> bool:
return True
@external
def adjust_twa_window(_twa_window: uint256):
access_control._check_role(RATE_ADMIN, msg.sender)
twa.adjust_twa_window(_twa_window)


@external
@view
def weight() -> uint256:
return twap.compute()
# TODO - should implement lower bound for weight, otherwise will be close to 0 at init TVL
return twa.compute()


@external
Expand All @@ -67,6 +80,5 @@ def process_rewards():
@external
def correct_distribution_rate(new_profit_max_unlock_time: uint256):
access_control._check_role(RATE_ADMIN, msg.sender)

extcall IVault(vault).setProfitMaxUnlockTime(new_profit_max_unlock_time)
extcall IVault(vault).process_report(self)
143 changes: 143 additions & 0 deletions contracts/TWA.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# pragma version ~=0.4

"""
@title Time Weighted Average (TWA) Calculator Vyper Module
@notice This contract stores snapshots of a tracked value at specific timestamps and computes the Time Weighted Average (TWA) over a defined time window.
@dev
- Snapshots Storage: Stores snapshots of a tracked value along with their timestamps in a dynamic array,
ensuring snapshots are only added if a minimum time interval (`min_snapshot_dt_seconds`) has passed since the last snapshot.
- TWA Calculation: Computes the TWA by iterating over the stored snapshots in reverse chronological order.
It uses the trapezoidal rule to calculate the weighted average of the tracked value over the specified time window (`twa_window`).
- Functions:
- `store_snapshot`: Internal function to store a new snapshot of the tracked value if the minimum time interval has passed.
!!!Wrapper must be implemented in importing contract.
- `compute_twa`: External view function that calculates and returns the TWA based on the stored snapshots.
- `get_len_snapshots`: External view function that returns the total number of snapshots stored.
- **Usage**: Ideal for tracking metrics like staked supply rates, token prices, or any other value that changes over time and requires averaging over a period.
"""

MAX_SNAPSHOTS: constant(uint256) = 10**18 # 31.7 billion years if snapshot every second

snapshots: public(DynArray[Snapshot, MAX_SNAPSHOTS])
min_snapshot_dt_seconds: public(uint256) # Minimum time between snapshots in seconds
twa_window: public(uint256) # Time window in seconds for TWA calculation
last_snapshot_timestamp: public(uint256) # Timestamp of the last snapshot (assigned in RewardsHandler)


struct Snapshot:
tracked_value: uint256 # In 1e18 precision
timestamp: uint256


@deploy
def __init__(_twa_window: uint256, _min_snapshot_dt_seconds: uint256):
self.twa_window = _twa_window # one week (in seconds)
self.min_snapshot_dt_seconds = _min_snapshot_dt_seconds # >=1s to prevent spamming


@internal
def store_snapshot(_value: uint256):
"""
@notice Stores a snapshot of the tracked value.
@param _value The value to store.
"""
if self.last_snapshot_timestamp + self.min_snapshot_dt_seconds <= block.timestamp:
self.last_snapshot_timestamp = block.timestamp
self.snapshots.append(
Snapshot(tracked_value=_value, timestamp=block.timestamp)
) # store the snapshot into the DynArray


@external
@view
def get_len_snapshots() -> uint256:
"""
@notice Returns the number of snapshots stored.
@return Number of snapshots.
"""
return len(self.snapshots)


@external
@view
def compute_twa() -> uint256:
"""
@notice External endpoint for _compute() function.
"""
return self.compute()


@internal
def adjust_twa_window(_new_window: uint256):
"""
@notice Adjusts the TWA window.
@param _new_window The new TWA window in seconds.
@dev Only callable by the importing contract.
"""
self.twa_window = _new_window


@internal
def adjust_min_snapshot_dt_seconds(_new_dt_seconds: uint256):
"""
@notice Adjusts the minimum snapshot time interval.
@param _new_dt_seconds The new minimum snapshot time interval in seconds.
@dev Only callable by the importing contract.
"""
self.min_snapshot_dt_seconds = _new_dt_seconds


@internal
@view
def compute() -> uint256:
"""
@notice Computes the TWA over the specified time window by iterating backwards over the snapshots.
@return The TWA for tracked value over the self.twa_window (10**18 decimals precision).
"""
num_snapshots: uint256 = len(self.snapshots)
if num_snapshots == 0:
return 0

time_window_start: uint256 = block.timestamp - self.twa_window

total_weighted_tracked_value: uint256 = 0
total_time: uint256 = 0

# Iterate backwards over all snapshots
index_array_end: uint256 = num_snapshots - 1
for i: uint256 in range(0, num_snapshots, bound=MAX_SNAPSHOTS): # i from 0 to (num_snapshots-1)
i_backwards: uint256 = index_array_end - i
current_snapshot: Snapshot = self.snapshots[i_backwards]
next_snapshot: Snapshot = current_snapshot
if i != 0: # If not the first iteration, get the next snapshot
next_snapshot = self.snapshots[i_backwards + 1]

interval_start: uint256 = current_snapshot.timestamp
# Adjust interval start if it is before the time window start
if interval_start < time_window_start:
interval_start = time_window_start

interval_end: uint256 = 0
if i == 0: # First iteration - we are on the last snapshot (i_backwards = num_snapshots - 1)
# For the last snapshot, interval end is block.timestamp
interval_end = block.timestamp
else:
# For other snapshots, interval end is the timestamp of the next snapshot
interval_end = next_snapshot.timestamp

if interval_end <= time_window_start:
break

time_delta: uint256 = interval_end - interval_start

# Interpolation using the trapezoidal rule
averaged_tracked_value: uint256 = (current_snapshot.tracked_value + next_snapshot.tracked_value) // 2

# Accumulate weighted rate and time
total_weighted_tracked_value += averaged_tracked_value * time_delta
total_time += time_delta

assert total_time > 0, "Zero total time!"
twa: uint256 = total_weighted_tracked_value // total_time

return twa
23 changes: 0 additions & 23 deletions contracts/TWAP.vy

This file was deleted.

Loading

0 comments on commit f85fabc

Please sign in to comment.