Skip to content

Commit

Permalink
WETH to yETH converter
Browse files Browse the repository at this point in the history
  • Loading branch information
0xkorin committed Dec 23, 2024
1 parent f1512c1 commit 152d7f4
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 0 deletions.
58 changes: 58 additions & 0 deletions contracts/Converter.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# pragma version 0.3.10
# pragma optimize gas
# pragma evm-version cancun

from vyper.interfaces import ERC20

interface Pool:
def exchange(i: int128, j: int128, _dx: uint256, _min_dy: uint256) -> uint256: nonpayable

weth: public(immutable(ERC20))
yeth: public(immutable(ERC20))
management: public(address)
pending_management: public(address)
operator: public(address)

@external
def __init__(_weth: address, _yeth: address):
weth = ERC20(_weth)
yeth = ERC20(_yeth)
self.management = msg.sender
self.operator = msg.sender

@external
def convert(_pool: address, _i: int128, _j: int128, _amount: uint256, _min_amount_out: uint256):
assert msg.sender == self.operator
assert _pool != empty(address)
assert _amount > 0
assert _min_amount_out >= _amount
assert weth.transferFrom(self.management, self, _amount, default_return_value=True)
assert weth.approve(_pool, _amount, default_return_value=True)
Pool(_pool).exchange(_i, _j, _amount, _min_amount_out)
amount_out: uint256 = yeth.balanceOf(self)
assert amount_out >= _min_amount_out
assert yeth.transfer(self.management, amount_out, default_return_value=True)

@external
def rescue(_token: address, _amount: uint256 = max_value(uint256)):
assert msg.sender == self.management
amount: uint256 = _amount
if _amount == max_value(uint256):
amount = ERC20(_token).balanceOf(self)
assert ERC20(_token).transfer(msg.sender, amount, default_return_value=True)

@external
def set_operator(_operator: address):
assert msg.sender == self.management
self.operator = _operator

@external
def set_management(_management: address):
assert msg.sender == self.management
self.pending_management = _management

@external
def accept_management():
assert msg.sender == self.pending_management
self.pending_management = empty(address)
self.management = msg.sender
26 changes: 26 additions & 0 deletions contracts/mocks/MockMaliciousPool.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# pragma version 0.3.10
# pragma optimize gas
# pragma evm-version cancun

from vyper.interfaces import ERC20

interface Pool:
def exchange(i: int128, j: int128, _dx: uint256, _min_dy: uint256) -> uint256: nonpayable
implements: Pool

weth: immutable(ERC20)
amount_out: uint256

@external
def __init__(_weth: address):
weth = ERC20(_weth)

@external
def set_amount_out(_amount_out: uint256):
self.amount_out = _amount_out

@external
def exchange(i: int128, j: int128, _dx: uint256, _min_dy: uint256) -> uint256:
# malicious pool only takes WETH and gives nothing in return
weth.transferFrom(msg.sender, self, _dx, default_return_value=True)
return self.amount_out
83 changes: 83 additions & 0 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import ape
from ape import Contract
import pytest

POOL = '0x69ACcb968B19a53790f43e57558F5E443A91aF22'
ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
UNIT = 10**18

@pytest.fixture
def operator(accounts):
return accounts[4]

@pytest.fixture
def weth():
return Contract('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')

@pytest.fixture
def yeth():
return Contract('0x1BED97CBC3c24A4fb5C069C6E311a967386131f7')

@pytest.fixture
def converter(project, deployer, operator, weth, yeth):
converter = project.Converter.deploy(weth, yeth, sender=deployer)
converter.set_operator(operator, sender=deployer)
return converter

def test_convert(deployer, operator, alice, weth, yeth, converter):
weth.deposit(value=3*UNIT, sender=deployer)
weth.approve(converter, UNIT, sender=deployer)

min_out = UNIT * 1001 // 1000

# permissioned
with ape.reverts():
converter.convert(POOL, 0, 1, UNIT, min_out, sender=alice)

# must be at least 1:1
with ape.reverts():
converter.convert(POOL, 0, 1, UNIT, UNIT - 1, sender=operator)

assert weth.balanceOf(deployer) == 3 * UNIT
assert yeth.balanceOf(deployer) == 0
assert weth.balanceOf(converter) == 0
assert yeth.balanceOf(converter) == 0
converter.convert(POOL, 0, 1, UNIT, min_out, sender=operator)
assert weth.balanceOf(deployer) == 2 * UNIT
assert yeth.balanceOf(deployer) > UNIT
assert weth.balanceOf(converter) == 0
assert yeth.balanceOf(converter) == 0

def test_malicious_pool(project, deployer, operator, weth, converter):
weth.deposit(value=3*UNIT, sender=deployer)
weth.approve(converter, UNIT, sender=deployer)
malicious = project.MockMaliciousPool.deploy(weth, sender=deployer)
with ape.reverts():
converter.convert(malicious, 0, 1, UNIT, UNIT, sender=operator)
malicious.set_amount_out(UNIT, sender=deployer)
with ape.reverts():
converter.convert(malicious, 0, 1, UNIT, UNIT, sender=operator)
converter.convert(POOL, 0, 1, UNIT, UNIT, sender=operator)

def test_set_operator(deployer, operator, alice, converter):
assert converter.operator() == operator
with ape.reverts():
converter.set_operator(alice, sender=alice)
converter.set_operator(alice, sender=deployer)
assert converter.operator() == alice

def test_set_management(deployer, alice, bob, converter):
assert converter.management() == deployer
assert converter.pending_management() == ZERO_ADDRESS
with ape.reverts():
converter.set_management(alice, sender=alice)
with ape.reverts():
converter.accept_management(sender=alice)
converter.set_management(alice, sender=deployer)
assert converter.management() == deployer
assert converter.pending_management() == alice
with ape.reverts():
converter.accept_management(sender=bob)
converter.accept_management(sender=alice)
assert converter.management() == alice
assert converter.pending_management() == ZERO_ADDRESS

0 comments on commit 152d7f4

Please sign in to comment.