Skip to content

Commit

Permalink
week06 solutions
Browse files Browse the repository at this point in the history
  • Loading branch information
juliusfrost committed May 4, 2023
1 parent 2432b91 commit d3a9bfe
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 26 deletions.
20 changes: 18 additions & 2 deletions src/week06/homework/fixed_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ class DatumSwap(PlutusData):
price: int


# Implement the swap protocol but avoid the double spending issue in exploitable_swap.py
def validator(datum: DatumSwap, redeemer: None, context: ScriptContext):
assert False # fix me!
tx_info = context.tx_info
count = 0
for txin in tx_info.inputs:
d = txin.resolved.datum
if isinstance(d, SomeOutputDatum):
count += 1
if isinstance(d, SomeOutputDatumHash):
count += 1
assert count == 1, "You can only include 1 uxto with a datum."
paid = False
for output in tx_info.outputs:
payment_cred: PubKeyCredential = output.address.payment_credential
if payment_cred.credential_hash == datum.beneficiary:
value_paid = output.value
ada_payed = value_paid.get(b"", {b"": 0}).get(b"", 0)
if ada_payed == datum.price:
paid = True
assert paid, "Hey! You have to pay the owner!"
217 changes: 193 additions & 24 deletions src/week06/homework/test_exploitable_swap.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
# Instructions:
# Implement your tests for exploitable_swap.py here.
# Test both single and double spending of the script UTxOs.
# Use this to test your design of fixed_swap.py
# You can use the method stubs below to guide your design of the tests, or create your own design.
# You should borrow the unit test ideas from test_negative_r_timed.py to complete the tests here.


from copy import deepcopy
from functools import cache
from types import ModuleType
from typing import Tuple

import opshin.prelude
import pycardano
import pytest
from opshin import build

import src.week05
from src.utils import network
from src.utils.mock import MockChainContext, MockUser
from src.week06 import lecture_dir, homework_dir
from src.week06.homework import fixed_swap
from src.week06.homework import fixed_swap_solved
from src.week06.lecture import exploitable_swap

# build script once outside test function
exploitable_swap_script = build(lecture_dir.joinpath("exploitable_swap.py"))
fixed_swap_script = build(homework_dir.joinpath("fixed_swap.py"))
fixed_swap_script = build(homework_dir.joinpath("fixed_swap_solved.py"))

script_dict = {
"exploitable_swap": [exploitable_swap_script, exploitable_swap],
"fixed_swap": [fixed_swap_script, fixed_swap],
"fixed_swap": [fixed_swap_script, fixed_swap_solved],
}


Expand All @@ -32,7 +30,50 @@ def create_nft(
user: MockUser,
token_name: str,
) -> Tuple[MockChainContext, pycardano.MultiAsset]:
pass
# get utxo
utxo_to_spend = None
for utxo in user.utxos():
if utxo.output.amount.coin > 3_000_000:
utxo_to_spend = utxo
break
assert utxo_to_spend is not None, "UTxO not found to spend!"

# create nft to sell
script_path = src.week05.lecture_dir.joinpath("nft.py")
oref = opshin.prelude.TxOutRef(
id=opshin.prelude.TxId(bytes(utxo_to_spend.input.transaction_id)),
idx=utxo_to_spend.input.index,
)
tn_bytes = bytes(token_name, encoding="utf-8")
plutus_script = build(script_path, oref, tn_bytes)

# Build the transaction
builder = pycardano.TransactionBuilder(context)
builder.add_minting_script(script=plutus_script, redeemer=pycardano.Redeemer(0))
nft = pycardano.MultiAsset.from_primitive(
{bytes(pycardano.plutus_script_hash(plutus_script)): {tn_bytes: 1}}
)
builder.mint = nft
builder.add_input(utxo_to_spend)
builder.add_output(
pycardano.TransactionOutput(
user.address,
amount=pycardano.Value(
coin=context.protocol_param.min_utxo, multi_asset=builder.mint
),
)
)

# Sign the transaction
signed_tx = builder.build_and_sign(
signing_keys=[user.signing_key],
change_address=user.address,
)

# Submit the transaction
context.submit_tx(signed_tx)

return context, nft


def lock_nft(
Expand All @@ -42,11 +83,25 @@ def lock_nft(
nft: pycardano.MultiAsset,
price: int,
) -> MockChainContext:
pass
# create datum
datum = exploitable_swap.DatumSwap(seller.verification_key.hash().payload, price)

# user 1 locks 2 ADA ("val") in validator
val = pycardano.Value(multi_asset=nft)
tx_builder = pycardano.TransactionBuilder(context)
tx_builder.add_input_address(seller.address)
tx_builder.add_output(
pycardano.TransactionOutput(script_address, amount=val, datum=datum)
)
tx = tx_builder.build_and_sign([seller.signing_key], change_address=seller.address)
context.submit_tx(tx)
return context


def create_buyer(context) -> Tuple[MockChainContext, MockUser]:
pass
buyer = MockUser(context)
buyer.fund(100_000_000)
return context, buyer


def unlock_nft(
Expand All @@ -57,19 +112,133 @@ def unlock_nft(
seller: MockUser,
price: int,
) -> MockChainContext:
pass
tx_builder = pycardano.TransactionBuilder(context)
tx_builder.add_input_address(buyer.address)
for utxo in context.utxos(script_address):
tx_builder.add_script_input(
utxo,
redeemer=pycardano.Redeemer(0),
script=plutus_script,
)
tx_builder.add_output(
pycardano.TransactionOutput(seller.address, pycardano.Value(coin=price))
)
tx_builder.validity_start = context.last_block_slot
tx_builder.ttl = tx_builder.validity_start + 1
tx = tx_builder.build_and_sign([buyer.signing_key], change_address=buyer.address)
context.submit_tx(tx)
return context


@cache
def setup_context(
plutus_script: pycardano.PlutusV2Script, script_module: ModuleType
) -> Tuple[MockChainContext, MockUser, pycardano.MultiAsset]:
pass


def test_normal_spending():
pass


def test_double_spending():
pass
context = MockChainContext()
context.opshin_scripts[plutus_script] = script_module.validator

seller = MockUser(context)
seller.fund(100_000_000) # 100 ADA

# run the minting contract to create the seller's nft
context, nft1 = create_nft(context, seller, "opshin is the best")
context, nft2 = create_nft(context, seller, "I love Python!")
return context, seller, nft1 + nft2


@pytest.mark.parametrize("script_name", ["exploitable_swap", "fixed_swap"])
def test_normal_spending(script_name):
plutus_script, script_module = script_dict[script_name]
context, seller, nft = deepcopy(setup_context(plutus_script, script_module))

script_hash = pycardano.plutus_script_hash(plutus_script)
script_address = pycardano.Address(script_hash, network=network)

# wait for a bit
context.wait(1000)

# seller locks nft for 5 ADA
price = 5_000_000
context = lock_nft(context, script_address, seller, nft, price)

# wait for a bit
context.wait(1000)

# create the buyer
context, buyer = create_buyer(context)

# buyer attempts to unlock nft by paying 5 ADA to seller
unlock_nft(context, plutus_script, script_address, buyer, seller, price)

# check that buyer has the nft
buyer_has_nft = False
for utxo in buyer.utxos():
if utxo.output.amount.multi_asset:
assert utxo.output.amount.multi_asset == nft
buyer_has_nft = True
assert buyer_has_nft

# check that there are no utxos at the script address
assert len(context.utxos(script_address)) == 0

# check that buyer paid only 5 ADA
balance = buyer.utxos()[0].output.amount.coin
assert 94_000_000 <= balance <= 95_000_000


@pytest.mark.parametrize(
["script_name", "validates"],
[
("exploitable_swap", True),
("fixed_swap", False),
],
)
def test_double_spending(script_name, validates):
plutus_script, script_module = script_dict[script_name]
context, seller, nft = deepcopy(setup_context(plutus_script, script_module))

script_hash = pycardano.plutus_script_hash(plutus_script)
script_address = pycardano.Address(script_hash, network=network)

# wait for a bit
context.wait(1000)

# seller locks 2 nfts for 5 ADA each
price = 5_000_000
list_nft = [
{bytes(pid): {bytes(name): 1}}
for pid, asset in nft.items()
for name in asset.keys()
]
for multiasset_primitive in list_nft:
single_nft = pycardano.MultiAsset.from_primitive(multiasset_primitive)
context = lock_nft(context, script_address, seller, single_nft, price)
context.wait(1000)

# create the buyer
context, buyer = create_buyer(context)

# buyer attempts to unlock all nfts at script address by paying 5 ADA to seller
try:
unlock_nft(context, plutus_script, script_address, buyer, seller, price)
unlocked = True
except (AssertionError, ValueError):
unlocked = False
assert unlocked == validates
if not validates:
return

# check that buyer has the nft
buyer_has_nft = False
for utxo in buyer.utxos():
if utxo.output.amount.multi_asset:
assert utxo.output.amount.multi_asset == nft
buyer_has_nft = True
assert buyer_has_nft

# check that there are no utxos at the script address
assert len(context.utxos(script_address)) == 0

# check that buyer paid only 5 ADA
balance = buyer.utxos()[0].output.amount.coin
assert 94_000_000 <= balance <= 95_000_000

0 comments on commit d3a9bfe

Please sign in to comment.