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

TraderJoeBurner for avalanche #28

Open
wants to merge 3 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
5 changes: 4 additions & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ jobs:
run: pip install -r requirements.txt

- name: Run Tests
run: brownie test tests/fork --network=mainnet-fork
run: brownie test tests/fork --network=mainnet-fork

- name: Run Avalanche Tests
run: brownie test tests/avalanche-fork --network=avax-main-fork
328 changes: 328 additions & 0 deletions contracts/burners/avalanche/TraderJoeBurner.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
# @version 0.2.15
"""
@title TraderJoe Burner
@notice Swap coins to USDC using TraderJoe, and send to receiver
"""

from vyper.interfaces import ERC20


interface UniswapV2Pair:
def token0() -> address:
view

def token1() -> address:
view

def factory() -> address:
view


interface UniswapV2Router02:
def removeLiquidity(
tokenA: address,
tokenB: address,
liquidity: uint256,
amountAMin: uint256,
amountBMin: uint256,
to: address,
deadline: uint256,
) -> uint256[2]:
nonpayable

def factory() -> address:
view


interface UniswapV2Factory:
def getPair(tokenA: address, tokenB: address) -> address:
view


is_approved: HashMap[address, HashMap[address, bool]]
receiver: public(address)
recovery: public(address)
is_killed: public(bool)
owner: public(address)
emergency_owner: public(address)
future_owner: public(address)
future_emergency_owner: public(address)


WAVAX: constant(address) = 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7

# USDC.e
USDC: constant(address) = 0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664
ROUTERS: constant(address[1]) = [
# traderjoe
0x60aE616a2155Ee3d9A68541Ba4544862310933d4,
]


@internal
def _swap_for_usdc(_coin: address, amount: uint256, router: address):
# vyper doesnt support dynamic array. build calldata manually
if _coin == WAVAX:
raw_call(
router,
concat(
method_id("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)"),
convert(amount, bytes32), # swap amount
EMPTY_BYTES32, # min expected
convert(160, bytes32), # offset pointer to path array
convert(self.receiver, bytes32), # receiver of the swap
convert(block.timestamp, bytes32), # swap deadline
convert(2, bytes32), # path length
convert(_coin, bytes32), # input token
convert(USDC, bytes32), # usdc (final output)
),
)
else:
raw_call(
router,
concat(
method_id("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)"),
convert(amount, bytes32), # swap amount
EMPTY_BYTES32, # min expected
convert(160, bytes32), # offset pointer to path array
convert(self.receiver, bytes32), # receiver of the swap
convert(block.timestamp, bytes32), # swap deadline
convert(3, bytes32), # path length
convert(_coin, bytes32), # input token
convert(WAVAX, bytes32), # wavax (intermediate swap)
convert(USDC, bytes32), # usdc (final output)
),
)


@internal
def _get_amounts_out(_coin: address, amount: uint256, router: address) -> uint256:
# vyper doesnt support dynamic array. build calldata manually
call_data: Bytes[256] = 0x00
if _coin == WAVAX:
call_data = concat(
method_id("getAmountsOut(uint256,address[])"),
convert(amount, bytes32),
convert(64, bytes32),
convert(2, bytes32),
convert(_coin, bytes32),
convert(USDC, bytes32),
)
else:
call_data = concat(
method_id("getAmountsOut(uint256,address[])"),
convert(amount, bytes32),
convert(64, bytes32),
convert(3, bytes32),
convert(_coin, bytes32),
convert(WAVAX, bytes32),
convert(USDC, bytes32),
)
response: Bytes[128] = raw_call(router, call_data, max_outsize=128)
response_bytes_start_index: uint256 = 0
if _coin == WAVAX:
response_bytes_start_index = 64
else:
response_bytes_start_index = 96
return convert(slice(response, response_bytes_start_index, 32), uint256)


@external
def __init__(_receiver: address, _recovery: address, _owner: address, _emergency_owner: address):
"""
@notice Contract constructor
@param _receiver Address that converted tokens are transferred to.
Should be set to an USDCBurner.
@param _recovery Address that tokens are transferred to during an
emergency token recovery.
@param _owner Owner address. Can kill the contract, recover tokens
and modify the recovery address.
@param _emergency_owner Emergency owner address. Can kill the contract
and recover tokens.
"""
self.receiver = _receiver
self.recovery = _recovery
self.owner = _owner
self.emergency_owner = _emergency_owner


@external
@nonreentrant("lock")
def burn(_coin: address) -> bool:
"""
@notice Receive `_coin` and swap it for USDC using TraderJoe
@param _coin Address of the coin being converted
@return bool success
"""
assert not self.is_killed # dev: is killed

# transfer coins from caller
amount: uint256 = ERC20(_coin).balanceOf(msg.sender)

if amount != 0:
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)

# get actual balance in case of transfer fee or pre-existing balance
amount = ERC20(_coin).balanceOf(self)

best_expected: uint256 = 0
router: address = ZERO_ADDRESS

# check the rates on traderjoe to see which is the better option
for addr in ROUTERS:
if _coin != WAVAX:
factory: address = UniswapV2Router02(addr).factory()
coin_wavax_pair: address = UniswapV2Factory(factory).getPair(_coin, WAVAX)
if coin_wavax_pair == ZERO_ADDRESS:
continue
expected: uint256 = self._get_amounts_out(_coin, amount, addr)
if expected > best_expected:
best_expected = expected
router = addr

assert router != ZERO_ADDRESS, "TraderJoe has liquidity pool for this token"
# make sure the router is approved to transfer the coin
if not self.is_approved[router][_coin]:
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(router, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.is_approved[router][_coin] = True
# swap for USDC on the best dex protocol
self._swap_for_usdc(_coin, amount, router)
return True


@external
def recover_balance(_coin: address) -> bool:
"""
@notice Recover ERC20 tokens from this contract
@dev Tokens are sent to the recovery address
@param _coin Token address
@return bool success
"""
assert msg.sender in [self.owner, self.emergency_owner] # dev: only owner

amount: uint256 = ERC20(_coin).balanceOf(self)
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("transfer(address,uint256)"),
convert(self.recovery, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)

return True


@external
def set_recovery(_recovery: address) -> bool:
"""
@notice Set the token recovery address
@param _recovery Token recovery address
@return bool success
"""
assert msg.sender == self.owner # dev: only owner
self.recovery = _recovery

return True


@external
def set_killed(_is_killed: bool) -> bool:
"""
@notice Set killed status for this contract
@dev When killed, the `burn` function cannot be called
@param _is_killed Killed status
@return bool success
"""
assert msg.sender in [self.owner, self.emergency_owner] # dev: only owner
self.is_killed = _is_killed

return True


@external
def commit_transfer_ownership(_future_owner: address) -> bool:
"""
@notice Commit a transfer of ownership
@dev Must be accepted by the new owner via `accept_transfer_ownership`
@param _future_owner New owner address
@return bool success
"""
assert msg.sender == self.owner # dev: only owner
self.future_owner = _future_owner

return True


@external
def accept_transfer_ownership() -> bool:
"""
@notice Accept a transfer of ownership
@return bool success
"""
assert msg.sender == self.future_owner # dev: only owner
self.owner = msg.sender

return True


@external
def commit_transfer_emergency_ownership(_future_owner: address) -> bool:
"""
@notice Commit a transfer of ownership
@dev Must be accepted by the new owner via `accept_transfer_ownership`
@param _future_owner New owner address
@return bool success
"""
assert msg.sender == self.emergency_owner # dev: only owner
self.future_emergency_owner = _future_owner

return True


@external
def accept_transfer_emergency_ownership() -> bool:
"""
@notice Accept a transfer of ownership
@return bool success
"""
assert msg.sender == self.future_emergency_owner # dev: only owner
self.emergency_owner = msg.sender

return True


@external
def set_receiver(_receiver: address) -> bool:
"""
@notice Set receiver
@param _receiver Receiver address
@return bool success
"""
assert msg.sender in [self.owner, self.emergency_owner] # dev: only owner
self.receiver = _receiver
return True
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
black==20.8b1
eth-brownie>=1.14.6,<2.0.0
black==21.11b1
eth-brownie==1.17.2
flake8==3.8.4
isort==5.7.0
brownie-token-tester>=0.2.2
brownie-token-tester==0.3.2
Loading