Skip to content

Commit

Permalink
Replacement tuning, auto-plunge on startup, manual testing with mcd-c…
Browse files Browse the repository at this point in the history
…li... (makerdao#82)

* removed unmaintained testchain artifacts

* break out of urn check upon Ctrl-C

* updated doco

* changed replacement period to three blocks

* allow consuming from reservoir when amount is equal to level of reservoir

* automatically plunge TX queue upon startup

* bugfix

* remove wait facility from the urn_cache test
  • Loading branch information
EdNoepel authored Sep 9, 2020
1 parent d6e6fde commit 26fb0a5
Show file tree
Hide file tree
Showing 31 changed files with 165 additions and 573 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ amount of time to wait. For illustration purposes, assume the queue can hold 12
reasonable. In this environment, a bid delay of 1.2 seconds might provide ample time for transactions at the front of
the queue to complete. [Etherscan.io](etherscan.io) can be used to view your account's pending transaction queue.

Upon startup, the keeper will clear its transaction queue. This helps recover from insufficiently-aggressive gas
configuration and reduces gas-wasting transactions.

#### Hardware and operating system resources

* The most expensive keepers are `flip` and `flop` keepers configured to `kick` new auctions.
Expand Down
2 changes: 1 addition & 1 deletion auction_keeper/gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def get_gas_price(self, time_elapsed: int) -> Optional[int]:
initial_price = int(round(fast_price * self.initial_multiplier))

return GeometricGasPrice(initial_price=initial_price,
every_secs=30,
every_secs=42,
coefficient=self.reactive_multiplier,
max_price=self.gas_maximum).get_gas_price(time_elapsed)

Expand Down
2 changes: 1 addition & 1 deletion auction_keeper/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def check_bid_cost(self, id: int, consume: Rad):
assert isinstance(id, int)
assert isinstance(consume, Rad)

if self.level > consume:
if self.level >= consume:
self.level -= consume
return True
else:
Expand Down
17 changes: 16 additions & 1 deletion auction_keeper/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from typing import Optional
from web3 import Web3

from pymaker import Address, web3_via_http
from pymaker import Address, get_pending_transactions, web3_via_http
from pymaker.deployment import DssDeployment
from pymaker.keys import register_keys
from pymaker.lifecycle import Lifecycle
Expand Down Expand Up @@ -306,6 +306,8 @@ def startup(self):

logging.info(f"Keeper will use {self.gas_price} for transactions and bids unless model instructs otherwise")

self.plunge()

def approve(self):
self.strategy.approve(gas_price=self.gas_price)
time.sleep(1)
Expand All @@ -318,6 +320,16 @@ def approve(self):
if self.collateral:
self.collateral.approve(self.our_address, gas_price=self.gas_price)

def plunge(self):
pending_txes = get_pending_transactions(self.web3)
if len(pending_txes) > 0:
while len(pending_txes) > 0:
logging.warning(f"Cancelling first of {len(pending_txes)} pending transactions")
pending_txes[0].cancel(gas_price=self.gas_price)
# After the synchronous cancel, wait to see if subsequent transactions get mined
time.sleep(28)
pending_txes = get_pending_transactions(self.web3)

def shutdown(self):
with self.auctions_lock:
del self.auctions
Expand Down Expand Up @@ -355,6 +367,9 @@ def check_vaults(self):
logging.debug(f"Evaluating {len(urns)} {self.ilk} urns to be bitten if any are unsafe")

for urn in urns.values():
if self.is_shutting_down():
return

if self.cat.can_bite(ilk, urn):
if self.arguments.bid_on_auctions and available_dai == Wad(0):
self.logger.warning(f"Skipping opportunity to bite urn {urn.address} "
Expand Down
40 changes: 40 additions & 0 deletions tests/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# auction-keeper manual testing

The collection of python and shell scripts herein may be used to test `auction-keeper`, `pymaker`'s auction facilities,
and relevant smart contracts in `dss`. Artifacts herein assume manual testing will be performed on Kovan.

## Dependencies

* Install `bc` and `jshon`
* Install [mcd-cli](https://github.com/makerdao/mcd-cli#installation)
* Configure your [environment for mcd-cli](https://github.com/makerdao/mcd-cli#configuration)
* Optionally, to avoid being prompted, set `ETH_PASSWORD` to the password for your private key.

## Testing flip auctions
The general test workflow is:
1. Procure some collateral in a vault owner account
2. Procure same Dai in a keeper account
3. Create "risky" vaults as close as possible to the liquidation ratio
4. Run `auction-keeper` configured to bite and bid
5. Periodically `drip` the `jug` to apply stability fees, causing the keeper to `bite`


### Creating a single risky vault
This can be done without `mcd-cli`, omitting most dependencies enumerated above.
`manual_test_create_unsafe_vault.py` creates one native urn for a particular account.


### Creating multiple risky vaults
From the root directory of this repository, with your virtual env sourced:
1. Set up your python path with `export PYTHONPATH=$PYTHONPATH:./lib/pymaker:./lib/pygasprice-client`.
2. Run `mcd -C kovan --ilk=ETH-A poke` (replacing `ETH-A` with the desired collateral type) to poke the spot price.
3. Run `python3 tests/manual_test_get_unsafe_vault_params.py` to determine `ink` (collateral) and `art` (Dai) for
creating a vault right at the liquidation ratio. You may pass collateral type as a parameter (defaults to `ETH-A`).
You may pass a desired amount of debt as a second parameter to test larger liquidations (defaults to dust limit).
4. Run `tests/create-vault.sh` passing these values of `ink` and `art` to create a risky vault. You'll likely want to
round up slightly to ensure there's enough collateral to generate the specified amount of Dai. Should the `draw`
fail because the vault would be unsafe, call `mcd -C kovan cdp [VAULT ID] draw [ART]` drawing slightly less Dai.
5. `drip` the `jug` periodically to apply stability fees, eventually creating an opportunity for the keeper to `bite`.

At any time, you may run `mcd -C kovan --ilk=ETH-A cdp ls` to see a list of your vaults and
`mcd -C kovan --ilk=ETH-A cdp [VAULT ID] urn` to check size and collateralization of each.
34 changes: 34 additions & 0 deletions tests/create-vault.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
set -e
export CURRENT_DIR=$(pwd)

mcd=mcd
#mcd=${CURRENT_DIR}/../../mcd-cli/bin/mcd # (to run mcd-cli from source)

if [ -z $2 ]; then
echo "Usage: ./create-vault.sh [INK] [ART]"
exit 1
fi
if [ -z $ETH_FROM ]; then
echo Please set ETH_FROM to the address to be used for vault creation
exit 1
fi
if [ -z $ETH_KEYSTORE ]; then
echo Please set ETH_KEYSTORE to the directory where your private keys reside
exit 1
fi
if [ -z $ETH_RPC_URL ]; then
echo Please set ETH_RPC_URL to your node\'s URI
exit 1
fi

# Amount of collateral to join and lock (Wad)
ink=${1:?}
# Amount of Dai to draw and exit (Wad)
art=${2:?}

#$mcd -C kovan wrap $ink > /dev/null
id=$($mcd -C kovan --ilk=ETH-A cdp open | sed -n "s/^Opened: cdp \([0-9]\+\)$/\1/ p") > /dev/null
$mcd -C kovan cdp $id lock $ink > /dev/null
$mcd -C kovan cdp $id draw $art > /dev/null
echo Created vault $id with ink=$ink and art=$art
55 changes: 55 additions & 0 deletions tests/manual_test_get_unsafe_vault_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import sys
from web3 import Web3, HTTPProvider

from pymaker.deployment import DssDeployment
from pymaker.numeric import Wad, Ray, Rad


# This script calculates the amount of collateral needed to produce a vault close to the liquidation ratio.
# Usage: python3 tests/manual_test_get_unsafe_vault_params.py [ILK] ([TARGET_ART])
# If [TARGET_ART] is omitted, the dust cutoff is used.


def r(value, decimals=1):
return round(float(value), decimals)


web3 = Web3(HTTPProvider(endpoint_uri=os.environ['ETH_RPC_URL'], request_kwargs={"timeout": 10}))
mcd = DssDeployment.from_node(web3)
collateral = mcd.collaterals[str(sys.argv[1])] if len(sys.argv) > 1 else mcd.collaterals['ETH-A']
ilk = collateral.ilk
target_art = Rad.from_number(float(sys.argv[2])) if len(sys.argv) > 2 else ilk.dust
osm_price = collateral.pip.peek()
print(f"{ilk.name} price={osm_price}, mat={r(mcd.spotter.mat(ilk),2)}, spot={ilk.spot}, rate={ilk.rate}")

# This accounts for several seconds of rate accumulation between time of calculation and the transaction being mined
flub_amount = Wad(1000)
ink_osm = Wad(target_art / Rad(osm_price) * Rad(mcd.spotter.mat(ilk)) * Rad(ilk.rate)) + flub_amount
ink = Wad(target_art / Rad(ilk.spot) * Rad(ilk.rate)) + flub_amount
if ink_osm != ink:
print(f"WARNING: ink required using OSM price ({ink_osm}) does not match ink required using spot price ({ink}).")
print(f"Please poke (mcd -C kovan --ilk={ilk.name} poke) to update spot price.")

# art_actual = Ray(ink) * ilk.spot / ilk.rate
art = Ray(target_art)
collat_ratio = Ray(ink) * Ray(osm_price) / (art * ilk.rate)

print(f"frob with ink={ink} {ilk.name}, art={art} Dai for {r(collat_ratio*100,6)}% collateralization")
11 changes: 1 addition & 10 deletions tests/manual_test_urn_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,14 @@
from_block = int(sys.argv[4]) if len(sys.argv) > 4 else 8928152


def wait(minutes_to_wait: int, uh: UrnHistory):
while minutes_to_wait > 0:
print(f"Testing cache for another {minutes_to_wait} minutes")
state_update_started = datetime.now()
uh.get_urns()
minutes_elapsed = int((datetime.now() - state_update_started).seconds / 60)
minutes_to_wait -= minutes_elapsed


# Retrieve data from chain
started = datetime.now()
print(f"Connecting to {sys.argv[1]}...")
uh = UrnHistory(web3, mcd, ilk, from_block, None, None)
urns_logs = uh.get_urns()
elapsed: timedelta = datetime.now() - started
print(f"Found {len(urns_logs)} urns from block {from_block} in {elapsed.seconds} seconds")
wait(30, uh)


# Retrieve data from Vulcanize
started = datetime.now()
Expand Down
3 changes: 1 addition & 2 deletions tests/random_bid.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

for line in sys.stdin:
signal = json.loads(line)
auction_id = signal['id']
assert isinstance(auction_id, int)
auction_id = int(signal['id'])

random.seed(a=auction_id)
price = max_price * random.random()
Expand Down
25 changes: 13 additions & 12 deletions tests/test_gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

GWEI = 1000000000
default_max_gas = 2000
every_secs = 42

class TestGasStrategy:
def test_ethgasstation(self, mcd, keeper_address):
Expand Down Expand Up @@ -87,10 +88,10 @@ def test_default_gas_config(self, web3, keeper_address):

default_initial_gas = get_node_gas_price(web3)
assert keeper.gas_price.get_gas_price(0) == default_initial_gas
assert keeper.gas_price.get_gas_price(31) == default_initial_gas * 1.125
assert keeper.gas_price.get_gas_price(61) == default_initial_gas * 1.125 ** 2
assert keeper.gas_price.get_gas_price(91) == default_initial_gas * 1.125 ** 3
assert keeper.gas_price.get_gas_price(30*80) == default_max_gas * GWEI
assert keeper.gas_price.get_gas_price(1 + every_secs) == default_initial_gas * 1.125
assert keeper.gas_price.get_gas_price(1 + every_secs * 2) == default_initial_gas * 1.125 ** 2
assert keeper.gas_price.get_gas_price(1 + every_secs * 3) == default_initial_gas * 1.125 ** 3
assert keeper.gas_price.get_gas_price(every_secs * 80) == default_max_gas * GWEI

def test_no_api_non_fixed(self, mcd, keeper_address):
c = mcd.collaterals['ETH-A']
Expand All @@ -105,10 +106,10 @@ def test_no_api_non_fixed(self, mcd, keeper_address):
f"--model ./bogus-model.sh"), web3=mcd.web3)
initial_amount = get_node_gas_price(mcd.web3)
assert keeper.gas_price.get_gas_price(0) == initial_amount
assert keeper.gas_price.get_gas_price(31) == initial_amount * reactive_multipler
assert keeper.gas_price.get_gas_price(61) == initial_amount * reactive_multipler ** 2
assert keeper.gas_price.get_gas_price(91) == initial_amount * reactive_multipler ** 3
assert keeper.gas_price.get_gas_price(30*12) == default_max_gas * GWEI
assert keeper.gas_price.get_gas_price(1 + every_secs) == initial_amount * reactive_multipler
assert keeper.gas_price.get_gas_price(1 + every_secs * 2) == initial_amount * reactive_multipler ** 2
assert keeper.gas_price.get_gas_price(1 + every_secs * 3) == initial_amount * reactive_multipler ** 3
assert keeper.gas_price.get_gas_price(every_secs * 12) == default_max_gas * GWEI

def test_fixed_with_explicit_max(self, web3, keeper_address):
keeper = AuctionKeeper(args=args(f"--eth-from {keeper_address} "
Expand All @@ -123,10 +124,10 @@ def test_fixed_with_explicit_max(self, web3, keeper_address):
assert keeper.gas_price.gas_maximum == 4000 * GWEI

assert keeper.gas_price.get_gas_price(0) == 100 * GWEI
assert keeper.gas_price.get_gas_price(31) == 100 * GWEI * 1.125
assert keeper.gas_price.get_gas_price(61) == 100 * GWEI * 1.125 ** 2
assert keeper.gas_price.get_gas_price(91) == 100 * GWEI * 1.125 ** 3
assert keeper.gas_price.get_gas_price(60*30) == 4000 * GWEI
assert keeper.gas_price.get_gas_price(1 + every_secs) == 100 * GWEI * 1.125
assert keeper.gas_price.get_gas_price(1 + every_secs * 2) == 100 * GWEI * 1.125 ** 2
assert keeper.gas_price.get_gas_price(1 + every_secs * 3) == 100 * GWEI * 1.125 ** 3
assert keeper.gas_price.get_gas_price(every_secs * 60) == 4000 * GWEI

def test_config_negative(self, web3, keeper_address):
with pytest.raises(SystemExit):
Expand Down
74 changes: 0 additions & 74 deletions tests/testchain/README.md

This file was deleted.

15 changes: 0 additions & 15 deletions tests/testchain/create-cdp.sh

This file was deleted.

Loading

0 comments on commit 26fb0a5

Please sign in to comment.