Skip to content

Commit 7a822ed

Browse files
committed
tests(Flashloan,Flashlend): add tests for flash lending functionality
1 parent 9d0ce9e commit 7a822ed

5 files changed

Lines changed: 251 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ readme = "README.md"
1010
requires-python = ">=3.9"
1111
dependencies = [
1212
"eth-ape>=0.8.32",
13-
"snekmate>=0.0.1",
1413
]
1514

1615
[[project.authors]]
@@ -24,6 +23,14 @@ packages = ["purse"]
2423

2524
[[tool.ape.plugins]]
2625
name = "vyper"
26+
[tool.ape.compile]
27+
exclude = [
28+
"accessories/*.storageLayout.json",
29+
]
30+
31+
[[tool.ape.dependencies]]
32+
pypi = "snekmate"
33+
version = "0.1.2"
2734

2835
[[tool.ape.plugins]]
2936
name = "foundry"

tests/conftest.py

Lines changed: 17 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
from ape.contracts import ContractInstance, ContractMethodHandler
35

@@ -17,6 +19,21 @@ def singleton(project, owner):
1719
return owner.deploy(project.Purse)
1820

1921

22+
@pytest.fixture(scope="session")
23+
def mocks():
24+
from ape import Project
25+
26+
return Project(
27+
Path(__file__).parent,
28+
config_override=dict(contracts_folder="mocks"),
29+
)
30+
31+
32+
@pytest.fixture(scope="session")
33+
def token(mocks, owner):
34+
return owner.deploy(mocks.MockToken)
35+
36+
2037
@pytest.fixture()
2138
def purse(singleton, owner):
2239
with owner.delegate_to(singleton) as purse:

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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import ape
2+
from ape.utils.misc import ZERO_ADDRESS
3+
import pytest
4+
5+
from eth_abi import abi
6+
7+
8+
@pytest.fixture(scope="module")
9+
def flashloan(project, owner):
10+
return owner.deploy(project.Flashloan)
11+
12+
13+
@pytest.fixture(scope="module")
14+
def flashlend(project, owner):
15+
return owner.deploy(project.Flashlend)
16+
17+
18+
@pytest.fixture()
19+
def purse(singleton, owner, flashloan, flashlend, encode_accessory_data):
20+
with owner.delegate_to(
21+
singleton,
22+
# NOTE: Add multicall as an accessory at the same time
23+
data=singleton.update_accessories.encode_input(
24+
encode_accessory_data(
25+
# ERC3156Receiver method
26+
flashloan.onFlashLoan,
27+
# ERC3156 methods
28+
flashlend.maxFlashLoan,
29+
flashlend.flashFee,
30+
flashlend.flashLoan,
31+
# Custom flashlend methods
32+
flashlend.setFlashFee,
33+
)
34+
),
35+
) as purse:
36+
purse.contract_type.abi.extend(flashloan.contract_type.abi)
37+
purse.contract_type.abi.extend(flashlend.contract_type.abi)
38+
yield purse
39+
40+
41+
def test_flashloan(purse, token, other):
42+
assert token.allowance(purse, other) == 0
43+
with ape.reverts("Flashloan:!authorized"):
44+
purse.onFlashLoan(purse, token, 1_000, 0, b"", sender=other)
45+
assert token.allowance(purse, other) == 0
46+
47+
tx = token.flashLoan(
48+
purse,
49+
token,
50+
1_000,
51+
# NOTE: View call is a no-op, should work
52+
abi.encode(["address","uint256"], [purse.address, 0]) + purse.maxFlashLoan.encode_input(token),
53+
sender=purse,
54+
)
55+
assert tx.events == [
56+
token.Transfer(sender=ZERO_ADDRESS, receiver=purse, value=1_000),
57+
token.Approval(owner=purse, spender=token, value=1_000),
58+
token.Transfer(sender=purse, receiver=ZERO_ADDRESS, value=1_000),
59+
]
60+
61+
62+
@pytest.fixture(scope="module")
63+
def flash_receiver(mocks, owner):
64+
return owner.deploy(mocks.MockFlashReceiver)
65+
66+
67+
def test_flashlend(purse, token, flash_receiver):
68+
assert purse.maxFlashLoan(token) == 0
69+
assert purse.flashFee(token, 1_000_000) == 0
70+
71+
with ape.reverts("Flashlend:!token-allowed"):
72+
purse.flashLoan(flash_receiver, token, token.balanceOf(purse) // 100, b"")
73+
74+
purse.setFlashFee(token, 1_000)
75+
76+
assert purse.maxFlashLoan(token) == token.balanceOf(purse)
77+
assert purse.flashFee(token, 1_000_000) == 1_000
78+
79+
token.mint(flash_receiver, int(1e18), sender=purse)
80+
purse.flashLoan(
81+
flash_receiver,
82+
token,
83+
prev_bal := token.balanceOf(purse),
84+
b"",
85+
)
86+
assert token.balanceOf(purse) == prev_bal + int(prev_bal * (1_000 / 1_000_000))

0 commit comments

Comments
 (0)