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 contract for offboarding Peg Keepers #72

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
182 changes: 182 additions & 0 deletions contracts/stabilizer/PegKeeperOffboarding.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# pragma version 0.3.10
"""
@title Peg Keeper Offboarding
@author Curve.Fi
@notice Allows PK to withdraw stablecoin without taking new debt
@license MIT
@custom:version 0.0.1
"""

version: public(constant(String[8])) = "0.0.1"


interface ERC20:
def balanceOf(_owner: address) -> uint256: view

interface StableSwap:
def get_p(i: uint256=0) -> uint256: view
def price_oracle(i: uint256=0) -> uint256: view

interface PegKeeper:
def pool() -> StableSwap: view
def debt() -> uint256: view
def IS_INVERSE() -> bool: view

event AddPegKeeper:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool

event RemovePegKeeper:
peg_keeper: PegKeeper

event SetFeeReceiver:
fee_receiver: address

event SetKilled:
is_killed: Killed
by: address

event SetAdmin:
admin: address

event SetEmergencyAdmin:
admin: address

struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool

enum Killed:
Provide # 1
Withdraw # 2

MAX_LEN: constant(uint256) = 32

peg_keepers: public(DynArray[PegKeeperInfo, MAX_LEN]) # PKs registered for offboarding
peg_keeper_i: HashMap[PegKeeper, uint256] # 1 + index of peg keeper in a list

fee_receiver: public(address)

is_killed: public(Killed)
admin: public(address)
emergency_admin: public(address)


@external
def __init__(_fee_receiver: address, _admin: address, _emergency_admin: address):
self.fee_receiver = _fee_receiver
self.admin = _admin
self.emergency_admin = _emergency_admin
log SetFeeReceiver(_fee_receiver)
log SetAdmin(_admin)
log SetEmergencyAdmin(_emergency_admin)


@external
@view
def provide_allowed(_pk: address=msg.sender) -> uint256:
"""
@notice Do not allow PegKeeper to provide more
@return Amount of stablecoin allowed to provide
"""
return 0



@external
@view
def withdraw_allowed(_pk: address=msg.sender) -> uint256:
"""
@notice Allow Peg Keeper to withdraw stablecoin from the pool
@return Amount of stablecoin allowed to withdraw
"""
if self.is_killed in Killed.Withdraw:
return 0
return max_value(uint256)


@external
def add_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN]):
assert msg.sender == self.admin

i: uint256 = len(self.peg_keepers)
for pk in _peg_keepers:
assert self.peg_keeper_i[pk] == empty(uint256) # dev: duplicate
pool: StableSwap = pk.pool()
success: bool = raw_call(
pool.address, _abi_encode(convert(0, uint256), method_id=method_id("price_oracle(uint256)")),
revert_on_failure=False
)
info: PegKeeperInfo = PegKeeperInfo({
peg_keeper: pk,
pool: pool,
is_inverse: pk.IS_INVERSE(),
include_index: success,
})
self.peg_keepers.append(info) # dev: too many pairs
i += 1
self.peg_keeper_i[pk] = i

log AddPegKeeper(info.peg_keeper, info.pool, info.is_inverse)


@external
def remove_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN]):
"""
@dev Most gas efficient will be sort pools reversely
"""
assert msg.sender == self.admin

peg_keepers: DynArray[PegKeeperInfo, MAX_LEN] = self.peg_keepers
for pk in _peg_keepers:
i: uint256 = self.peg_keeper_i[pk] - 1 # dev: pool not found
max_n: uint256 = len(peg_keepers) - 1
if i < max_n:
peg_keepers[i] = peg_keepers[max_n]
self.peg_keeper_i[peg_keepers[i].peg_keeper] = 1 + i

peg_keepers.pop()
self.peg_keeper_i[pk] = empty(uint256)
log RemovePegKeeper(pk)

self.peg_keepers = peg_keepers


@external
def set_fee_receiver(_fee_receiver: address):
"""
@notice Set new PegKeeper's profit receiver
"""
assert msg.sender == self.admin
self.fee_receiver = _fee_receiver
log SetFeeReceiver(_fee_receiver)


@external
def set_killed(_is_killed: Killed):
"""
@notice Pause/unpause Peg Keepers
@dev 0 unpause, 1 provide, 2 withdraw, 3 everything
"""
assert msg.sender in [self.admin, self.emergency_admin]
self.is_killed = _is_killed
log SetKilled(_is_killed, msg.sender)


@external
def set_admin(_admin: address):
# We are not doing commit / apply because the owner will be a voting DAO anyway
# which has vote delays
assert msg.sender == self.admin
self.admin = _admin
log SetAdmin(_admin)


@external
def set_emergency_admin(_admin: address):
assert msg.sender == self.admin
self.emergency_admin = _admin
log SetEmergencyAdmin(_admin)
112 changes: 112 additions & 0 deletions tests/stableborrow/stabilize/unitary/test_pk_offboarding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import boa
import pytest

pytestmark = pytest.mark.usefixtures(
"add_initial_liquidity",
"provide_token_to_peg_keepers",
"mint_alice",
)


ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
ADMIN_ACTIONS_DEADLINE = 3 * 86400


@pytest.fixture(scope="module")
def offboarding(stablecoin, receiver, admin, peg_keepers):
hr = boa.load(
'contracts/stabilizer/PegKeeperOffboarding.vy',
stablecoin, ZERO_ADDRESS, receiver, admin, admin
)
with boa.env.prank(admin):
for peg_keeper in peg_keepers:
peg_keeper.set_new_regulator(hr)
return hr


def test_offboarding(offboarding, stablecoin, peg_keepers, swaps, receiver, admin, alice, peg_keeper_updater):
with boa.env.prank(admin):
for peg_keeper in peg_keepers:
stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10 ** 18}")

for peg_keeper, swap in zip(peg_keepers, swaps):
assert offboarding.provide_allowed(peg_keeper) == 0
assert offboarding.withdraw_allowed(peg_keeper) == 2**256 - 1

# Able to withdraw
with boa.env.prank(alice):
swap.add_liquidity([0, 10**20], 0)
balances = [swap.balances(0), swap.balances(1)]

with boa.env.prank(peg_keeper_updater):
assert peg_keeper.update()

new_balances = [swap.balances(0), swap.balances(1)]
assert new_balances[0] == balances[0]
assert new_balances[1] < balances[1]


def test_set_killed(offboarding, peg_keepers, admin, stablecoin):
peg_keeper = peg_keepers[0]
stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10 ** 18}")
with boa.env.prank(admin):
assert offboarding.is_killed() == 0

assert offboarding.provide_allowed(peg_keeper) == 0
assert offboarding.withdraw_allowed(peg_keeper) == 2**256 - 1

offboarding.set_killed(1)
assert offboarding.is_killed() == 1

assert offboarding.provide_allowed(peg_keeper) == 0
assert offboarding.withdraw_allowed(peg_keeper) == 2 ** 256 - 1

offboarding.set_killed(2)
assert offboarding.is_killed() == 2

assert offboarding.provide_allowed(peg_keeper) == 0
assert offboarding.withdraw_allowed(peg_keeper) == 0

offboarding.set_killed(3)
assert offboarding.is_killed() == 3

assert offboarding.provide_allowed(peg_keeper) == 0
assert offboarding.withdraw_allowed(peg_keeper) == 0


def test_admin(reg, admin, alice, agg, receiver):
# initial parameters
assert reg.fee_receiver() == receiver
assert reg.emergency_admin() == admin
assert reg.is_killed() == 0
assert reg.admin() == admin

# third party has no access
with boa.env.prank(alice):
with boa.reverts():
reg.set_fee_receiver(alice)
with boa.reverts():
reg.set_emergency_admin(alice)
with boa.reverts():
reg.set_killed(1)
with boa.reverts():
reg.set_admin(alice)

# admin has access
with boa.env.prank(admin):
reg.set_fee_receiver(alice)
assert reg.fee_receiver() == alice

reg.set_emergency_admin(alice)
assert reg.emergency_admin() == alice

reg.set_killed(1)
assert reg.is_killed() == 1
with boa.env.prank(alice): # emergency admin
reg.set_killed(2)
assert reg.is_killed() == 2

reg.set_admin(alice)
assert reg.admin() == alice