Skip to content

Commit

Permalink
Missed player mint achievement with signature.
Browse files Browse the repository at this point in the history
  • Loading branch information
Kellan Wampler committed Mar 6, 2024
1 parent 623cb78 commit 92194f6
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 6 deletions.
94 changes: 94 additions & 0 deletions cli/web3cli/AchievementFacet.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ def verify_contract(self):
contract_class = contract_from_build(self.contract_name)
contract_class.publish_source(self.contract)

def achievement_version(
self, block_number: Optional[Union[str, int]] = "latest"
) -> Any:
self.assert_contract_is_instantiated()
return self.contract.achievementVersion.call(block_identifier=block_number)

def admin_batch_mint_to_inventory(
self, subject_token_ids: List, pool_ids: List, transaction_config
) -> Any:
Expand Down Expand Up @@ -212,6 +218,30 @@ def max_amount_of_item_in_slot(
slot, item_type, item_address, item_pool_id, block_identifier=block_number
)

def mint_hash(
self,
subject_token_id: int,
pool_id: int,
block_number: Optional[Union[str, int]] = "latest",
) -> Any:
self.assert_contract_is_instantiated()
return self.contract.mintHash.call(
subject_token_id, pool_id, block_identifier=block_number
)

def mint_to_inventory(
self,
subject_token_id: int,
pool_id: int,
signer: ChecksumAddress,
signature: bytes,
transaction_config,
) -> Any:
self.assert_contract_is_instantiated()
return self.contract.mintToInventory(
subject_token_id, pool_id, signer, signature, transaction_config
)

def num_slots(self, block_number: Optional[Union[str, int]] = "latest") -> Any:
self.assert_contract_is_instantiated()
return self.contract.numSlots.call(block_identifier=block_number)
Expand Down Expand Up @@ -389,6 +419,13 @@ def handle_verify_contract(args: argparse.Namespace) -> None:
print(result)


def handle_achievement_version(args: argparse.Namespace) -> None:
network.connect(args.network)
contract = AchievementFacet(args.address)
result = contract.achievement_version(block_number=args.block_number)
print(result)


def handle_admin_batch_mint_to_inventory(args: argparse.Namespace) -> None:
network.connect(args.network)
contract = AchievementFacet(args.address)
Expand Down Expand Up @@ -548,6 +585,33 @@ def handle_max_amount_of_item_in_slot(args: argparse.Namespace) -> None:
print(result)


def handle_mint_hash(args: argparse.Namespace) -> None:
network.connect(args.network)
contract = AchievementFacet(args.address)
result = contract.mint_hash(
subject_token_id=args.subject_token_id,
pool_id=args.pool_id,
block_number=args.block_number,
)
print(result)


def handle_mint_to_inventory(args: argparse.Namespace) -> None:
network.connect(args.network)
contract = AchievementFacet(args.address)
transaction_config = get_transaction_config(args)
result = contract.mint_to_inventory(
subject_token_id=args.subject_token_id,
pool_id=args.pool_id,
signer=args.signer_arg,
signature=args.signature,
transaction_config=transaction_config,
)
print(result)
if args.verbose:
print(result.info())


def handle_num_slots(args: argparse.Namespace) -> None:
network.connect(args.network)
contract = AchievementFacet(args.address)
Expand Down Expand Up @@ -699,6 +763,10 @@ def generate_cli() -> argparse.ArgumentParser:
add_default_arguments(verify_contract_parser, False)
verify_contract_parser.set_defaults(func=handle_verify_contract)

achievement_version_parser = subcommands.add_parser("achievement-version")
add_default_arguments(achievement_version_parser, False)
achievement_version_parser.set_defaults(func=handle_achievement_version)

admin_batch_mint_to_inventory_parser = subcommands.add_parser(
"admin-batch-mint-to-inventory"
)
Expand Down Expand Up @@ -842,6 +910,32 @@ def generate_cli() -> argparse.ArgumentParser:
func=handle_max_amount_of_item_in_slot
)

mint_hash_parser = subcommands.add_parser("mint-hash")
add_default_arguments(mint_hash_parser, False)
mint_hash_parser.add_argument(
"--subject-token-id", required=True, help="Type: uint256", type=int
)
mint_hash_parser.add_argument(
"--pool-id", required=True, help="Type: uint256", type=int
)
mint_hash_parser.set_defaults(func=handle_mint_hash)

mint_to_inventory_parser = subcommands.add_parser("mint-to-inventory")
add_default_arguments(mint_to_inventory_parser, True)
mint_to_inventory_parser.add_argument(
"--subject-token-id", required=True, help="Type: uint256", type=int
)
mint_to_inventory_parser.add_argument(
"--pool-id", required=True, help="Type: uint256", type=int
)
mint_to_inventory_parser.add_argument(
"--signer-arg", required=True, help="Type: address"
)
mint_to_inventory_parser.add_argument(
"--signature", required=True, help="Type: bytes", type=bytes_argument_type
)
mint_to_inventory_parser.set_defaults(func=handle_mint_to_inventory)

num_slots_parser = subcommands.add_parser("num-slots")
add_default_arguments(num_slots_parser, False)
num_slots_parser.set_defaults(func=handle_num_slots)
Expand Down
131 changes: 126 additions & 5 deletions cli/web3cli/test_achievement.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
from brownie import accounts, network, web3 as web3_client, ZERO_ADDRESS
from brownie.exceptions import VirtualMachineError
from brownie.network import chain
from eth_account._utils.signing import sign_message_hash
import eth_keys
from hexbytes import HexBytes
from moonworm.watch import _fetch_events_chunk

from . import AchievementFacet, MockERC721, MockTerminus, inventory_events
from .core import achievement_gogogo

MAX_UINT = 2**256 - 1

def sign_message(message_hash, signer):
eth_private_key = eth_keys.keys.PrivateKey(HexBytes(signer.private_key))
message_hash_bytes = HexBytes(message_hash)
_, _, _, signed_message_bytes = sign_message_hash(
eth_private_key, message_hash_bytes
)
return signed_message_bytes.hex()

class AchievementTestCase(unittest.TestCase):
@classmethod
Expand All @@ -33,16 +43,16 @@ def setUpClass(cls) -> None:
cls.owner = accounts[0]
cls.owner_tx_config = {"from": cls.owner}

cls.admin = accounts[1]
cls.admin = accounts.add()
cls.admin_tx_config = {"from": cls.admin}

cls.player_1 = accounts[2]
cls.player_1 = accounts[1]
cls.player_2 = accounts[2]
cls.player_3 = accounts[3]

cls.random_person = accounts[3]
cls.random_person = accounts.add()
cls.random_person_tx_config = {"from": cls.random_person}

cls.player_2 = accounts[4]
cls.player_3 = accounts[5]

cls.nft = MockERC721.MockERC721(None)
cls.nft.deploy(cls.owner_tx_config)
Expand Down Expand Up @@ -292,4 +302,115 @@ def test_admin_can_mint_multiple_achievements_to_multiple_players(self):
# EquippedItem is (ItemType, ItemAddress, ItemTokenID, Amount)
self.assertEqual(equipped_item_2, (1155, self.achievement.address, achievement_pool_2, 1))

class TestPlayerFlow(AchievementTestCase):
def test_player_can_mint_to_inventory_with_signature(self):
# Mint token to player
player_balance_0 = self.nft.balance_of(self.player_1.address)
token_id = self.nft.total_supply() + 1
self.nft.mint(self.player_1.address, token_id, self.owner_tx_config)
player_balance_1 = self.nft.balance_of(self.player_1.address)

self.assertEqual(player_balance_1, player_balance_0 + 1)

# Create slot
metadata_uri = "http://www.example.com/test_achievement_4"
self.achievement.create_achievement_slot(metadata_uri, self.admin_tx_config)
achievement_pool = self.terminus.total_pools()

self.assertEqual(self.terminus.uri(achievement_pool), metadata_uri)

# Sign message
message_hash = self.achievement.mint_hash(token_id, achievement_pool)
signed_message = sign_message(message_hash, self.admin)

# Mint to inventory
self.achievement.mint_to_inventory(token_id, achievement_pool, self.admin.address, signed_message, {"from": self.player_1})

self.assertEqual(self.terminus.balance_of(self.achievement.address, achievement_pool), 1)
equipped_item = self.achievement.get_equipped_item(token_id, achievement_pool)

# EquippedItem is (ItemType, ItemAddress, ItemTokenID, Amount)
self.assertEqual(equipped_item, (1155, self.achievement.address, achievement_pool, 1))

def test_player_cannot_mint_with_signature_from_invalid_signer(self):
# Mint token to player
player_balance_0 = self.nft.balance_of(self.player_1.address)
token_id = self.nft.total_supply() + 1
self.nft.mint(self.player_1.address, token_id, self.owner_tx_config)
player_balance_1 = self.nft.balance_of(self.player_1.address)

self.assertEqual(player_balance_1, player_balance_0 + 1)

# Create slot
metadata_uri = "http://www.example.com/test_achievement_5"
self.achievement.create_achievement_slot(metadata_uri, self.admin_tx_config)
achievement_pool = self.terminus.total_pools()

self.assertEqual(self.terminus.uri(achievement_pool), metadata_uri)

# Sign message
message_hash = self.achievement.mint_hash(token_id, achievement_pool)
signed_message = sign_message(message_hash, self.random_person)

# Attempt mint "unauthorized signer"
with self.assertRaises(VirtualMachineError):
self.achievement.mint_to_inventory(token_id, achievement_pool, self.random_person.address, signed_message, {"from": self.player_1})

# Attempt to lie about signer "invalid signature"
with self.assertRaises(VirtualMachineError):
self.achievement.mint_to_inventory(token_id, achievement_pool, self.admin.address, signed_message, {"from": self.player_1})

def test_player_cannot_mint_with_invalid_signature(self):
# Mint token to player
player_balance_0 = self.nft.balance_of(self.player_1.address)
token_id = self.nft.total_supply() + 1
self.nft.mint(self.player_1.address, token_id, self.owner_tx_config)
player_balance_1 = self.nft.balance_of(self.player_1.address)

self.assertEqual(player_balance_1, player_balance_0 + 1)

# Create slot
metadata_uri_1 = "http://www.example.com/test_achievement_6"
self.achievement.create_achievement_slot(metadata_uri_1, self.admin_tx_config)
achievement_pool_1 = self.terminus.total_pools()

self.assertEqual(self.terminus.uri(achievement_pool_1), metadata_uri_1)

# Create slot
metadata_uri_2 = "http://www.example.com/test_achievement_7"
self.achievement.create_achievement_slot(metadata_uri_2, self.admin_tx_config)
achievement_pool_2 = self.terminus.total_pools()

self.assertEqual(self.terminus.uri(achievement_pool_2), metadata_uri_2)

# Sign message with achievement 1
message_hash = self.achievement.mint_hash(token_id, achievement_pool_1)
signed_message = sign_message(message_hash, self.admin)

# Attempt to mint achievement 2
with self.assertRaises(VirtualMachineError):
self.achievement.mint_to_inventory(token_id, achievement_pool_2, self.admin.address, signed_message, {"from": self.player_1})

def test_player_cannot_mint_achievement_for_another_players_token(self):
# Mint token to player 2
player_balance_0 = self.nft.balance_of(self.player_2.address)
token_id = self.nft.total_supply() + 1
self.nft.mint(self.player_2.address, token_id, self.owner_tx_config)
player_balance_1 = self.nft.balance_of(self.player_2.address)

self.assertEqual(player_balance_1, player_balance_0 + 1)

# Create slot
metadata_uri = "http://www.example.com/test_achievement_8"
self.achievement.create_achievement_slot(metadata_uri, self.admin_tx_config)
achievement_pool = self.terminus.total_pools()

self.assertEqual(self.terminus.uri(achievement_pool), metadata_uri)

# Sign message
message_hash = self.achievement.mint_hash(token_id, achievement_pool)
signed_message = sign_message(message_hash, self.admin)

# Attempt to mint achievement with player 1 (owned by player 2)
with self.assertRaises(VirtualMachineError):
self.achievement.mint_to_inventory(token_id, achievement_pool, self.admin.address, signed_message, {"from": self.player_1})
66 changes: 65 additions & 1 deletion contracts/inventory/AchievementFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {Slot, EquippedItem} from "../interfaces/IInventory.sol";
import {ITerminus} from "../interfaces/ITerminus.sol";
import {LibTerminus} from "../terminus/LibTerminus.sol";
import {LibDiamondMoonstream} from "../diamond/libraries/LibDiamondMoonstream.sol";
import {LibSignatures} from "../diamond/libraries/LibSignatures.sol";

import "@openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {IERC721} from "@openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {ERC1155Receiver} from "@openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Receiver.sol";
import {SignatureChecker} from "@openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sol";

library LibAchievement {
bytes32 constant STORAGE_POSITION =
Expand Down Expand Up @@ -58,6 +60,9 @@ contract AchievementFacet is InventoryFacet {
function initialize(address subjectAddress) external {
LibDiamondMoonstream.enforceIsContractOwner();

// Set up server side signing parameters for EIP712
LibSignatures._setEIP712Parameters("Moonstream Achievement", "0.0.1");

LibAchievement.AchievementStorage storage acs = LibAchievement
.achievementStorage();

Expand Down Expand Up @@ -89,6 +94,16 @@ contract AchievementFacet is InventoryFacet {
emit NewSubjectAddress(subjectAddress);
}

function achievementVersion()
public
view
returns (string memory, string memory)
{
LibSignatures.SignaturesStorage storage ss = LibSignatures
.signaturesStorage();
return (ss.name, ss.version);
}

// Grants an account the role of admin.
function grantAdminPrivilege(address admin) external {
LibDiamondMoonstream.enforceIsContractOwner();
Expand Down Expand Up @@ -240,4 +255,53 @@ contract AchievementFacet is InventoryFacet {
_mintToInventory(subjectTokenIds[i], poolIds[i]);
}
}

function mintHash(
uint256 subjectTokenID,
uint256 poolID
) public view returns (bytes32) {
bytes32 structHash = keccak256(
abi.encode(
keccak256(
"MintToInventoryMessage(uint256 subjectTokenID,uint256 poolID)"
),
subjectTokenID,
poolID
)
);
return LibSignatures._hashTypedDataV4(structHash);
}

function mintToInventory(
uint256 subjectTokenID,
uint256 poolID,
address signer,
bytes memory signature
) public {
LibInventory.InventoryStorage storage istore = LibInventory
.inventoryStorage();
IERC721 nft = IERC721(istore.ContractERC721Address);

require(
msg.sender == nft.ownerOf(subjectTokenID),
"Achievement.mintToInventory: Message sender is not owner of subject token."
);

require(
_checkIsAdmin(signer),
"Achievement.mintToInventory: unauthorized signer"
);

bytes32 mintMessageHash = mintHash(subjectTokenID, poolID);
require(
SignatureChecker.isValidSignatureNow(
signer,
mintMessageHash,
signature
),
"Achievement.mintToInventory: invalid signature"
);

_mintToInventory(subjectTokenID, poolID);
}
}

0 comments on commit 92194f6

Please sign in to comment.