Skip to content

Commit fd666b5

Browse files
committed
tests(Flashloan,Flashlend): add tests for flash lending functionality
1 parent 282a9c8 commit fd666b5

5 files changed

Lines changed: 236 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ include = [ "purse" ]
4242

4343
[[tool.ape.plugins]]
4444
name = "vyper"
45+
[tool.ape.compile]
46+
exclude = [
47+
"accessories/*.storageLayout.json",
48+
]
49+
50+
[[tool.ape.dependencies]]
51+
pypi = "snekmate"
52+
version = "0.1.2"
4553

4654
[[tool.ape.plugins]]
4755
name = "foundry"

tests/conftest.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from pathlib import Path
2+
13
import pytest
24

35
from purse import Accessory, Purse
@@ -18,6 +20,21 @@ def singleton(project, owner):
1820
return owner.deploy(project.Purse)
1921

2022

23+
@pytest.fixture(scope="session")
24+
def mocks():
25+
from ape import Project
26+
27+
return Project(
28+
Path(__file__).parent,
29+
config_override=dict(contracts_folder="mocks"),
30+
)
31+
32+
33+
@pytest.fixture(scope="session")
34+
def token(mocks, owner):
35+
return owner.deploy(mocks.MockToken)
36+
37+
2138
@pytest.fixture()
2239
def purse(singleton, owner):
2340
# NOTE: Empty purse for testing purposes
@@ -39,6 +56,16 @@ def sponsor(project, owner):
3956
return Accessory(owner.deploy(project.Sponsor))
4057

4158

59+
@pytest.fixture(scope="module")
60+
def flashloan(project, owner):
61+
return Accessory(owner.deploy(project.Flashloan))
62+
63+
64+
@pytest.fixture(scope="module")
65+
def flashlend(project, owner):
66+
return Accessory(owner.deploy(project.Flashlend))
67+
68+
4269
@pytest.fixture(scope="session")
4370
def dummy(compilers, owner):
4471
SRC = """# pragma version 0.4.1

tests/mocks/MockFlashReceiver.vy

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from ethereum.ercs import IERC20
2+
3+
4+
interface IERC3156FlashBorrower:
5+
def onFlashLoan(
6+
initiator: address,
7+
token: IERC20,
8+
amount: uint256,
9+
fee: uint256,
10+
data: Bytes[65535],
11+
) -> bytes32: nonpayable
12+
13+
14+
implements: IERC3156FlashBorrower
15+
16+
17+
@external
18+
def onFlashLoan(
19+
initiator: address,
20+
token: IERC20,
21+
amount: uint256,
22+
fee: uint256,
23+
data: Bytes[65535],
24+
) -> bytes32:
25+
# NOTE: Make sure this contract has enough balance prior to calling
26+
extcall token.approve(msg.sender, amount + fee)
27+
return keccak256("ERC3156FlashBorrower.onFlashLoan")

tests/mocks/MockToken.vy

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from ethereum.ercs import IERC20
2+
3+
implements: IERC20
4+
5+
totalSupply: public(uint256)
6+
balanceOf: public(HashMap[address, uint256])
7+
allowance: public(HashMap[address, HashMap[address, uint256]])
8+
9+
10+
@deploy
11+
def __init__():
12+
self.totalSupply = 100 * 10 ** 18
13+
self.balanceOf[msg.sender] = 100 * 10 ** 18
14+
15+
16+
@external
17+
def transfer(receiver: address, amount: uint256) -> bool:
18+
self.balanceOf[msg.sender] -= amount
19+
self.balanceOf[receiver] += amount
20+
log IERC20.Transfer(sender=msg.sender, receiver=receiver, value=amount)
21+
return True
22+
23+
24+
@external
25+
def approve(spender: address, amount: uint256) -> bool:
26+
self.allowance[msg.sender][spender] = amount
27+
log IERC20.Approval(owner=msg.sender, spender=spender, value=amount)
28+
return True
29+
30+
31+
@external
32+
def transferFrom(owner: address, receiver: address, amount: uint256) -> bool:
33+
self.allowance[owner][msg.sender] -= amount
34+
self.balanceOf[owner] -= amount
35+
self.balanceOf[receiver] += amount
36+
log IERC20.Transfer(sender=owner, receiver=receiver, value=amount)
37+
return True
38+
39+
40+
@external
41+
def mint(receiver: address, amount: uint256):
42+
self.totalSupply += amount
43+
self.balanceOf[receiver] += amount
44+
log IERC20.Transfer(sender=empty(address), receiver=receiver, value=amount)
45+
46+
47+
interface IERC3156FlashBorrower:
48+
def onFlashLoan(
49+
initiator: address,
50+
token: IERC20,
51+
amount: uint256,
52+
fee: uint256,
53+
data: Bytes[65535],
54+
) -> bytes32: nonpayable
55+
56+
57+
interface IERC3156:
58+
def maxFlashLoan(token: IERC20) -> uint256: view
59+
def flashFee(token: IERC20, amount: uint256) -> uint256: view
60+
def flashLoan(
61+
receiver: IERC3156FlashBorrower,
62+
token: IERC20,
63+
amount: uint256,
64+
data: Bytes[65535],
65+
) -> bool: nonpayable
66+
67+
68+
implements: IERC3156
69+
70+
71+
@view
72+
@external
73+
def maxFlashLoan(token: IERC20) -> uint256:
74+
if token.address == self:
75+
return max_value(uint256) - self.totalSupply
76+
77+
return 0
78+
79+
80+
@view
81+
@external
82+
def flashFee(token: IERC20, amount: uint256) -> uint256:
83+
return 0
84+
85+
86+
@external
87+
def flashLoan(
88+
receiver: IERC3156FlashBorrower,
89+
token: IERC20,
90+
amount: uint256,
91+
data: Bytes[65535],
92+
) -> bool:
93+
assert token.address == self
94+
95+
# Send our tokens to receiver
96+
self.totalSupply += amount
97+
self.balanceOf[receiver.address] += amount
98+
log IERC20.Transfer(sender=empty(address), receiver=receiver.address, value=amount)
99+
100+
assert (
101+
# NOTE: `msg.sender` is original caller of delegatecall
102+
extcall receiver.onFlashLoan(msg.sender, IERC20(self), amount, 0, data)
103+
# NOTE: Magic value per ERC-3156
104+
== keccak256("ERC3156FlashBorrower.onFlashLoan")
105+
), "Flashloan receiver not valid"
106+
107+
# Get our tokens back (mimic a real flash lender)
108+
self.allowance[receiver.address][self] -= amount
109+
self.totalSupply -= amount
110+
self.balanceOf[receiver.address] -= amount
111+
log IERC20.Transfer(sender=receiver.address, receiver=empty(address), value=amount)
112+
113+
return True

tests/test_flashloans.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import ape
2+
from ape.utils.misc import ZERO_ADDRESS
3+
import pytest
4+
5+
from eth_abi import abi
6+
7+
from purse import Purse
8+
9+
10+
@pytest.fixture()
11+
def purse(singleton, owner, flashloan, flashlend):
12+
return Purse.initialize(owner, flashloan, flashlend, singleton=singleton)
13+
14+
15+
def test_flashloan(purse, token, other):
16+
assert token.allowance(purse, other) == 0
17+
with ape.reverts("Flashloan:!authorized"):
18+
purse.onFlashLoan(purse, token, 1_000, 0, b"", sender=other)
19+
assert token.allowance(purse, other) == 0
20+
21+
tx = token.flashLoan(
22+
purse,
23+
token,
24+
1_000,
25+
# NOTE: View call is a no-op, should work
26+
abi.encode(["address", "uint256"], [purse.address, 0])
27+
+ purse.maxFlashLoan.encode_input(token),
28+
sender=purse,
29+
)
30+
assert tx.events == [
31+
token.Transfer(sender=ZERO_ADDRESS, receiver=purse, value=1_000),
32+
token.Approval(owner=purse, spender=token, value=1_000),
33+
token.Transfer(sender=purse, receiver=ZERO_ADDRESS, value=1_000),
34+
]
35+
36+
37+
@pytest.fixture(scope="module")
38+
def flash_receiver(mocks, owner):
39+
return owner.deploy(mocks.MockFlashReceiver)
40+
41+
42+
def test_flashlend(purse, token, flash_receiver):
43+
assert purse.maxFlashLoan(token) == 0
44+
assert purse.flashFee(token, 1_000_000) == 0
45+
46+
with ape.reverts("Flashlend:!token-allowed"):
47+
purse.flashLoan(flash_receiver, token, token.balanceOf(purse) // 100, b"")
48+
49+
purse.setFlashFee(token, 10_000) # 10k mbps = 10 bps = 0.1%
50+
51+
assert purse.maxFlashLoan(token) == token.balanceOf(purse)
52+
assert purse.flashFee(token, 10_000_000) == 10_000
53+
54+
token.mint(flash_receiver, int(1e18), sender=purse)
55+
purse.flashLoan(
56+
flash_receiver,
57+
token,
58+
prev_bal := token.balanceOf(purse),
59+
b"",
60+
)
61+
assert token.balanceOf(purse) == prev_bal + int(prev_bal * (10_000 / 10_000_000))

0 commit comments

Comments
 (0)