Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic discount #220

Merged
merged 6 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
steps:
- uses: actions/checkout@v1
- uses: ApeWorX/github-action@v1
with:
python-version: 3.9

- name: Compile contracts
run: ape compile --size
Expand Down
26 changes: 22 additions & 4 deletions contracts/Options.vy
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ DISCOUNT: constant(uint256) = 2

OYFI: immutable(ERC20)
YFI: immutable(ERC20)
VEYFI: immutable(address)
CURVE_POOL: immutable(CurvePoolInterface)
PRICE_FEED: immutable(AggregatorV3Interface)

DISCOUNT_TABLE: constant(uint16[500]) = [9156, 9149, 9142, 9134, 9127, 9119, 9112, 9104, 9096, 9089, 9081, 9073, 9065, 9057, 9049, 9041, 9033, 9024, 9016, 9008, 8999, 8991, 8982, 8974, 8965, 8956, 8947, 8939, 8930, 8921, 8912, 8902, 8893, 8884, 8875, 8865, 8856, 8846, 8837, 8827, 8817, 8807, 8797, 8787, 8777, 8767, 8757, 8747, 8736, 8726, 8716, 8705, 8694, 8684, 8673, 8662, 8651, 8640, 8629, 8618, 8607, 8595, 8584, 8573, 8561, 8549, 8538, 8526, 8514, 8502, 8490, 8478, 8466, 8454, 8441, 8429, 8417, 8404, 8391, 8379, 8366, 8353, 8340, 8327, 8314, 8301, 8287, 8274, 8261, 8247, 8233, 8220, 8206, 8192, 8178, 8164, 8150, 8136, 8121, 8107, 8093, 8078, 8063, 8049, 8034, 8019, 8004, 7989, 7974, 7959, 7943, 7928, 7913, 7897, 7881, 7866, 7850, 7834, 7818, 7802, 7786, 7770, 7753, 7737, 7720, 7704, 7687, 7670, 7654, 7637, 7620, 7603, 7585, 7568, 7551, 7533, 7516, 7498, 7481, 7463, 7445, 7427, 7409, 7391, 7373, 7355, 7336, 7318, 7300, 7281, 7262, 7244, 7225, 7206, 7187, 7168, 7149, 7130, 7110, 7091, 7072, 7052, 7033, 7013, 6993, 6974, 6954, 6934, 6914, 6894, 6873, 6853, 6833, 6813, 6792, 6772, 6751, 6730, 6710, 6689, 6668, 6647, 6626, 6605, 6584, 6563, 6542, 6521, 6499, 6478, 6456, 6435, 6413, 6392, 6370, 6348, 6326, 6304, 6283, 6261, 6239, 6217, 6194, 6172, 6150, 6128, 6105, 6083, 6061, 6038, 6016, 5993, 5971, 5948, 5925, 5903, 5880, 5857, 5834, 5811, 5789, 5766, 5743, 5720, 5697, 5674, 5651, 5628, 5604, 5581, 5558, 5535, 5512, 5488, 5465, 5442, 5419, 5395, 5372, 5348, 5325, 5302, 5278, 5255, 5231, 5208, 5185, 5161, 5138, 5114, 5091, 5067, 5044, 5020, 4997, 4973, 4950, 4926, 4903, 4879, 4856, 4832, 4809, 4786, 4762, 4739, 4715, 4692, 4668, 4645, 4622, 4598, 4575, 4552, 4528, 4505, 4482, 4459, 4436, 4412, 4389, 4366, 4343, 4320, 4297, 4274, 4251, 4228, 4205, 4182, 4159, 4137, 4114, 4091, 4068, 4046, 4023, 4001, 3978, 3956, 3933, 3911, 3888, 3866, 3844, 3822, 3799, 3777, 3755, 3733, 3711, 3689, 3668, 3646, 3624, 3602, 3581, 3559, 3538, 3516, 3495, 3474, 3452, 3431, 3410, 3389, 3368, 3347, 3326, 3305, 3284, 3264, 3243, 3223, 3202, 3182, 3161, 3141, 3121, 3101, 3081, 3061, 3041, 3021, 3001, 2981, 2962, 2942, 2923, 2903, 2884, 2865, 2846, 2827, 2808, 2789, 2770, 2751, 2732, 2714, 2695, 2677, 2658, 2640, 2622, 2604, 2586, 2568, 2550, 2532, 2514, 2497, 2479, 2462, 2444, 2427, 2410, 2392, 2375, 2358, 2342, 2325, 2308, 2291, 2275, 2258, 2242, 2226, 2209, 2193, 2177, 2161, 2145, 2130, 2114, 2098, 2083, 2067, 2052, 2037, 2022, 2006, 1991, 1976, 1962, 1947, 1932, 1918, 1903, 1889, 1874, 1860, 1846, 1832, 1818, 1804, 1790, 1776, 1762, 1749, 1735, 1722, 1709, 1695, 1682, 1669, 1656, 1643, 1630, 1617, 1605, 1592, 1579, 1567, 1555, 1542, 1530, 1518, 1506, 1494, 1482, 1470, 1458, 1447, 1435, 1424, 1412, 1401, 1390, 1378, 1367, 1356, 1345, 1334, 1324, 1313, 1302, 1292, 1281, 1271, 1260, 1250, 1240, 1229, 1219, 1209, 1199, 1189, 1180, 1170, 1160, 1151, 1141, 1132, 1122, 1113, 1104, 1094, 1085, 1076, 1067, 1058, 1049, 1041, 1032, 1023, 1015, 1006, 998, 989, 981, 973, 964, 956, 948, 940, 932, 924, 916, 909]
DISCOUNT_GRANULARITY : constant(uint256) = 500
DISCOUNT_NUMERATOR: constant(uint256) = 10_000
# @dev Returns the address of the current owner.
owner: public(address)
# @dev Returns the address of the pending owner.
Expand Down Expand Up @@ -51,9 +54,10 @@ event OwnershipTransferred:


@external
def __init__(yfi: address, o_yfi: address, owner: address, price_feed: address, curve_pool: address):
def __init__(yfi: address, o_yfi: address, ve_yfi: address, owner: address, price_feed: address, curve_pool: address):
YFI = ERC20(yfi)
OYFI = ERC20(o_yfi)
VEYFI = ve_yfi
PRICE_FEED = AggregatorV3Interface(price_feed)
CURVE_POOL = CurvePoolInterface(curve_pool)
self._transfer_ownership(owner)
Expand Down Expand Up @@ -95,8 +99,22 @@ def eth_required(amount: uint256) -> uint256:
@view
def _eth_required(amount: uint256) -> uint256:
eth_per_yfi: uint256 = self._get_latest_price()

return amount * eth_per_yfi / PRICE_DENOMINATOR / DISCOUNT

total_supply: uint256 = YFI.totalSupply()
total_locked: uint256 = YFI.balanceOf(VEYFI)
Copy link
Contributor

@storming0x storming0x Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be manipulated by simply transferring tokens to veYFI, not sure theres an attack possible tho, is there another function to check total locked in veYFI contract that uses veyfi internal
state instead?

Copy link
Contributor Author

@pandadefi pandadefi Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only loose YFI if you send it to veYFI. A YFI token sent by accident to veYFI can be considered locked forever. The contract has supply that counts the deposited YFIs.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a potential attack vector transferring tokens, but I would like to confirm the edge case where total_locked = total_supply. In this case afaik total_locked * DISCOUNT_GRANULARITY / total_supply would be 200 (DISCOUNT_GRANULARITY), and DISCOUNT_TABLE[200] would fail, since DISCOUNT_TABLE has 200 items. Please CMIIW.

Copy link
Contributor Author

@pandadefi pandadefi Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't possible, if 100% of the YFI are locked, we won't sell YFI in option.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true. I was thinking how to break the code :)

discount: uint256 = 0
if total_locked == 0:
discount = convert(DISCOUNT_TABLE[0], uint256)
else:
discount = convert(DISCOUNT_TABLE[total_locked * DISCOUNT_GRANULARITY / total_supply], uint256)

return amount * eth_per_yfi / PRICE_DENOMINATOR * discount / DISCOUNT_NUMERATOR


@external
@view
def get_latest_price() -> uint256:
return self._get_latest_price()


@internal
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
black==22.3.0
eth-ape==0.6.1
eth-ape==0.6.2
36 changes: 36 additions & 0 deletions scripts/discount_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import matplotlib.pyplot as plt
import math

# discounts = []
# MAX = 100
# NUMERATOR = 1000

# for i in range(1,MAX):
# discounts.append(1.0 / (1.0 + (9.9999 * (math.e ** (4.6969 * (i / MAX - 1))))))

# fig, ax = plt.subplots()

# ax.plot(range(1, 100), discounts, linewidth=2.0)

# plt.show()

discounts = []
MAX = 500
NUMERATOR = 10000
import math

for i in range(1, MAX + 1):
print(i)
d = 1.0 / (1.0 + (9.9999 * (math.e ** (4.6969 * (i / MAX - 1)))))
discounts.append(int(d * NUMERATOR))

total_supply = 10**20
usd_price = 7_500
print("YFI price: {usd_price} USD")
for i in range(1, 100):
total_locked = 10**18 * i
discount = discounts[int(total_locked * MAX / total_supply)]
price = 10**18 * (NUMERATOR - discount) / NUMERATOR
print(
f"with {i} percent locked, 1oYFI can be exchange for 1YFI at price: {price / 10**18 * usd_price} usd"
)
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ def o_yfi(accounts, project):


@pytest.fixture(scope="session")
def options(accounts, project, yfi, o_yfi):
def options(accounts, project, yfi, o_yfi, ve_yfi):
yield project.Options.deploy(
yfi,
o_yfi,
ve_yfi,
accounts[0],
"0x7c5d4F8345e66f68099581Db340cd65B078C41f4",
"0xc26b89a667578ec7b3f11b2f98d6fd15c07c54ba",
Expand Down
30 changes: 6 additions & 24 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,21 @@ def gov(accounts):
yield accounts[0]


@pytest.fixture(scope="session")
def whale_amount():
yield 10**22


@pytest.fixture(scope="session")
def whale(accounts, yfi, whale_amount):
@pytest.fixture()
def whale(accounts):
a = accounts[1]
yfi.mint(a, whale_amount, sender=a)
yield a


@pytest.fixture(scope="session")
def shark_amount():
yield 10**20


@pytest.fixture(scope="session")
def shark(accounts, yfi, shark_amount):
@pytest.fixture()
def shark(accounts):
a = accounts[2]
yfi.mint(a, shark_amount, sender=a)
yield a


@pytest.fixture(scope="session")
def fish_amount():
yield 10**18


@pytest.fixture(scope="session")
def fish(accounts, yfi, fish_amount):
@pytest.fixture()
def fish(accounts):
a = accounts[3]
yfi.mint(a, fish_amount, sender=a)
yield a


Expand Down
26 changes: 17 additions & 9 deletions tests/functional/test_gauge_reward_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ def test_gauge_yfi_distribution_full_rewards(
o_yfi,
ve_yfi,
whale,
whale_amount,
create_vault,
create_gauge,
gov,
ve_yfi_o_yfi_pool,
):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 4 * 3600 * 24 * 365, sender=whale
Expand Down Expand Up @@ -94,14 +95,14 @@ def test_boost_lock(
o_yfi,
ve_yfi,
whale,
whale_amount,
create_vault,
create_gauge,
panda,
gov,
ve_yfi_o_yfi_pool,
):

whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 4 * 3600 * 24 * 365, sender=whale
Expand Down Expand Up @@ -150,13 +151,14 @@ def test_gauge_get_reward_for(
o_yfi,
ve_yfi,
whale,
whale_amount,
shark,
create_vault,
create_gauge,
gov,
ve_yfi_o_yfi_pool,
):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 4 * 3600 * 24 * 365, sender=whale
Expand Down Expand Up @@ -193,12 +195,13 @@ def test_deposit_for(
ve_yfi,
whale,
shark,
whale_amount,
create_vault,
create_gauge,
ve_yfi_o_yfi_pool,
gov,
):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 4 * 3600 * 24 * 365, sender=whale
Expand Down Expand Up @@ -250,12 +253,13 @@ def test_withdraw(
o_yfi,
ve_yfi,
whale,
whale_amount,
create_vault,
create_gauge,
gov,
ve_yfi_o_yfi_pool,
):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 4 * 3600 * 24 * 365, sender=whale
Expand Down Expand Up @@ -286,7 +290,9 @@ def test_withdraw(
assert gauge.queuedRewards() == 0


def test_kick(create_vault, create_gauge, whale_amount, panda, yfi, ve_yfi, whale, gov):
def test_kick(create_vault, create_gauge, panda, yfi, ve_yfi, whale, gov):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
lp_amount = 10**18
vault = create_vault()
gauge = create_gauge(vault)
Expand Down Expand Up @@ -328,12 +334,13 @@ def withdraw_for(
ve_yfi,
whale,
panda,
whale_amount,
create_vault,
create_gauge,
gov,
ve_yfi_o_yfi_pool,
):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 4 * 3600 * 24 * 365, sender=whale
Expand Down Expand Up @@ -372,12 +379,13 @@ def transfer(
ve_yfi,
whale,
panda,
whale_amount,
create_vault,
create_gauge,
gov,
ve_yfi_o_yfi_pool,
):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 4 * 3600 * 24 * 365, sender=whale
Expand Down
36 changes: 34 additions & 2 deletions tests/functional/test_options.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
import ape
import pytest
from ape import chain
import math

SLIPPAGE_TOLERANCE = 3
SLIPPAGE_DENOMINATOR = 1000
AMOUNT = 10**18
# contract constants
DISCOUNT_TABLE = []
MAX = 500
NUMERATOR = 10000

for i in range(1, MAX + 1):
d = 1.0 / (1.0 + (9.9999 * (math.e ** (4.6969 * (i / MAX - 1)))))
DISCOUNT_TABLE.append(int(d * NUMERATOR))

def test_exercise(o_yfi, yfi, options, gov, panda):
yfi.mint(options, AMOUNT, sender=gov)

@pytest.mark.parametrize("percent_locked", [1, 10, 40, 70])
def test_exercise(o_yfi, yfi, ve_yfi, options, gov, panda, percent_locked):
# Lock tokens to reach the targeted percentage of locked tokens
assert yfi.totalSupply() == 0
total_to_mint = 10**22
yfi.mint(gov, total_to_mint, sender=gov)
to_lock = int(total_to_mint * percent_locked / 100)
yfi.approve(ve_yfi, to_lock, sender=gov)
ve_yfi.modify_lock(
to_lock, chain.blocks.head.timestamp + 3600 * 24 * 14, sender=gov
)

yfi.transfer(options, AMOUNT, sender=gov)
o_yfi.mint(panda, AMOUNT, sender=gov)
estimate = options.eth_required(AMOUNT)
assert (
pytest.approx(
int(
options.get_latest_price()
* DISCOUNT_TABLE[percent_locked * 5]
/ NUMERATOR
)
)
== estimate
)
o_yfi.approve(options, AMOUNT, sender=panda)
assert yfi.balanceOf(panda) == 0
options.exercise(AMOUNT, sender=panda, value=estimate)
Expand Down
8 changes: 6 additions & 2 deletions tests/functional/test_ve_yfi_rewards.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"


def test_ve_yfi_claim(yfi, ve_yfi, whale, whale_amount, ve_yfi_rewards, gov):
def test_ve_yfi_claim(yfi, ve_yfi, whale, ve_yfi_rewards, gov):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 86400 * 365, sender=whale
Expand Down Expand Up @@ -34,7 +36,9 @@ def test_ve_yfi_claim(yfi, ve_yfi, whale, whale_amount, ve_yfi_rewards, gov):
assert yfi.balanceOf(whale) == rewards


def test_ve_yfi_claim_for(yfi, ve_yfi, whale, fish, whale_amount, ve_yfi_rewards, gov):
def test_ve_yfi_claim_for(yfi, ve_yfi, whale, fish, ve_yfi_rewards, gov):
whale_amount = 10**22
yfi.mint(whale, whale_amount, sender=whale)
yfi.approve(ve_yfi, whale_amount, sender=whale)
ve_yfi.modify_lock(
whale_amount, chain.pending_timestamp + 3600 * 24 * 365, sender=whale
Expand Down