diff --git a/python/fullcount/Fullcount.py b/python/fullcount/Fullcount.py index d9c99a9c..c1875d31 100644 --- a/python/fullcount/Fullcount.py +++ b/python/fullcount/Fullcount.py @@ -225,15 +225,20 @@ def staked_session( arg1, arg2, block_identifier=block_number ) - def abort_session(self, session_id: int, transaction_config) -> Any: + def trusted_executors( + self, + arg1: ChecksumAddress, + arg2: ChecksumAddress, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: self.assert_contract_is_instantiated() - return self.contract.abortSession(session_id, transaction_config) + return self.contract.TrustedExecutors.call( + arg1, arg2, block_identifier=block_number + ) - def at_bat_hash( - self, at_bat_id: int, block_number: Optional[Union[str, int]] = "latest" - ) -> Any: + def abort_session(self, session_id: int, transaction_config) -> Any: self.assert_contract_is_instantiated() - return self.contract.atBatHash.call(at_bat_id, block_identifier=block_number) + return self.contract.abortSession(session_id, transaction_config) def commit_pitch( self, session_id: int, signature: bytes, transaction_config @@ -271,6 +276,17 @@ def get_session( self.assert_contract_is_instantiated() return self.contract.getSession.call(session_id, block_identifier=block_number) + def is_executor_for_player( + self, + executor: ChecksumAddress, + player: ChecksumAddress, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.isExecutorForPlayer.call( + executor, player, block_identifier=block_number + ) + def join_session( self, session_id: int, @@ -372,6 +388,12 @@ def session_progress( session_id, block_identifier=block_number ) + def set_trusted_executor( + self, executor: ChecksumAddress, approved: bool, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setTrustedExecutor(executor, approved, transaction_config) + def start_at_bat( self, nft_address: ChecksumAddress, @@ -398,6 +420,25 @@ def start_session( nft_address, token_id, role, require_signature, transaction_config ) + def submit_trusted_at_bat( + self, + pitcher_nft: tuple, + batter_nft: tuple, + pitches: List, + swings: List, + proposed_outcome: int, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.submitTrustedAtBat( + pitcher_nft, + batter_nft, + pitches, + swings, + proposed_outcome, + transaction_config, + ) + def swing_hash( self, nonce: int, @@ -411,12 +452,44 @@ def swing_hash( nonce, kind, vertical, horizontal, block_identifier=block_number ) + def trusted_at_bat_hash( + self, + at_bat_id: str, + nft_address: ChecksumAddress, + token_id: int, + role: int, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.trustedAtBatHash.call( + at_bat_id, nft_address, token_id, role, block_identifier=block_number + ) + def unstake_nft( self, nft_address: ChecksumAddress, token_id: int, transaction_config ) -> Any: self.assert_contract_is_instantiated() return self.contract.unstakeNFT(nft_address, token_id, transaction_config) + def verify_trusted_at_bat_signature( + self, + at_bat_id: str, + nft_address: ChecksumAddress, + token_id: int, + role: int, + signature: bytes, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.verifyTrustedAtBatSignature.call( + at_bat_id, + nft_address, + token_id, + role, + signature, + block_identifier=block_number, + ) + def get_transaction_config(args: argparse.Namespace) -> Dict[str, Any]: signer = network.accounts.load(args.sender, args.password) @@ -634,25 +707,25 @@ def handle_staked_session(args: argparse.Namespace) -> None: print(result) -def handle_abort_session(args: argparse.Namespace) -> None: +def handle_trusted_executors(args: argparse.Namespace) -> None: network.connect(args.network) contract = Fullcount(args.address) - transaction_config = get_transaction_config(args) - result = contract.abort_session( - session_id=args.session_id, transaction_config=transaction_config + result = contract.trusted_executors( + arg1=args.arg1, arg2=args.arg2, block_number=args.block_number ) print(result) - if args.verbose: - print(result.info()) -def handle_at_bat_hash(args: argparse.Namespace) -> None: +def handle_abort_session(args: argparse.Namespace) -> None: network.connect(args.network) contract = Fullcount(args.address) - result = contract.at_bat_hash( - at_bat_id=args.at_bat_id, block_number=args.block_number + transaction_config = get_transaction_config(args) + result = contract.abort_session( + session_id=args.session_id, transaction_config=transaction_config ) print(result) + if args.verbose: + print(result.info()) def handle_commit_pitch(args: argparse.Namespace) -> None: @@ -717,6 +790,15 @@ def handle_get_session(args: argparse.Namespace) -> None: print(result) +def handle_is_executor_for_player(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = Fullcount(args.address) + result = contract.is_executor_for_player( + executor=args.executor, player=args.player, block_number=args.block_number + ) + print(result) + + def handle_join_session(args: argparse.Namespace) -> None: network.connect(args.network) contract = Fullcount(args.address) @@ -831,6 +913,20 @@ def handle_session_progress(args: argparse.Namespace) -> None: print(result) +def handle_set_trusted_executor(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = Fullcount(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_trusted_executor( + executor=args.executor, + approved=args.approved, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + def handle_start_at_bat(args: argparse.Namespace) -> None: network.connect(args.network) contract = Fullcount(args.address) @@ -863,6 +959,23 @@ def handle_start_session(args: argparse.Namespace) -> None: print(result.info()) +def handle_submit_trusted_at_bat(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = Fullcount(args.address) + transaction_config = get_transaction_config(args) + result = contract.submit_trusted_at_bat( + pitcher_nft=args.pitcher_nft, + batter_nft=args.batter_nft, + pitches=args.pitches, + swings=args.swings, + proposed_outcome=args.proposed_outcome, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + def handle_swing_hash(args: argparse.Namespace) -> None: network.connect(args.network) contract = Fullcount(args.address) @@ -876,6 +989,19 @@ def handle_swing_hash(args: argparse.Namespace) -> None: print(result) +def handle_trusted_at_bat_hash(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = Fullcount(args.address) + result = contract.trusted_at_bat_hash( + at_bat_id=args.at_bat_id, + nft_address=args.nft_address, + token_id=args.token_id, + role=args.role, + block_number=args.block_number, + ) + print(result) + + def handle_unstake_nft(args: argparse.Namespace) -> None: network.connect(args.network) contract = Fullcount(args.address) @@ -890,6 +1016,20 @@ def handle_unstake_nft(args: argparse.Namespace) -> None: print(result.info()) +def handle_verify_trusted_at_bat_signature(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = Fullcount(args.address) + result = contract.verify_trusted_at_bat_signature( + at_bat_id=args.at_bat_id, + nft_address=args.nft_address, + token_id=args.token_id, + role=args.role, + signature=args.signature, + block_number=args.block_number, + ) + print(result) + + def generate_cli() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="CLI for Fullcount") parser.set_defaults(func=lambda _: parser.print_help()) @@ -1016,6 +1156,12 @@ def generate_cli() -> argparse.ArgumentParser: ) staked_session_parser.set_defaults(func=handle_staked_session) + trusted_executors_parser = subcommands.add_parser("trusted-executors") + add_default_arguments(trusted_executors_parser, False) + trusted_executors_parser.add_argument("--arg1", required=True, help="Type: address") + trusted_executors_parser.add_argument("--arg2", required=True, help="Type: address") + trusted_executors_parser.set_defaults(func=handle_trusted_executors) + abort_session_parser = subcommands.add_parser("abort-session") add_default_arguments(abort_session_parser, True) abort_session_parser.add_argument( @@ -1023,13 +1169,6 @@ def generate_cli() -> argparse.ArgumentParser: ) abort_session_parser.set_defaults(func=handle_abort_session) - at_bat_hash_parser = subcommands.add_parser("at-bat-hash") - add_default_arguments(at_bat_hash_parser, False) - at_bat_hash_parser.add_argument( - "--at-bat-id", required=True, help="Type: uint256", type=int - ) - at_bat_hash_parser.set_defaults(func=handle_at_bat_hash) - commit_pitch_parser = subcommands.add_parser("commit-pitch") add_default_arguments(commit_pitch_parser, True) commit_pitch_parser.add_argument( @@ -1079,6 +1218,16 @@ def generate_cli() -> argparse.ArgumentParser: ) get_session_parser.set_defaults(func=handle_get_session) + is_executor_for_player_parser = subcommands.add_parser("is-executor-for-player") + add_default_arguments(is_executor_for_player_parser, False) + is_executor_for_player_parser.add_argument( + "--executor", required=True, help="Type: address" + ) + is_executor_for_player_parser.add_argument( + "--player", required=True, help="Type: address" + ) + is_executor_for_player_parser.set_defaults(func=handle_is_executor_for_player) + join_session_parser = subcommands.add_parser("join-session") add_default_arguments(join_session_parser, True) join_session_parser.add_argument( @@ -1199,6 +1348,16 @@ def generate_cli() -> argparse.ArgumentParser: ) session_progress_parser.set_defaults(func=handle_session_progress) + set_trusted_executor_parser = subcommands.add_parser("set-trusted-executor") + add_default_arguments(set_trusted_executor_parser, True) + set_trusted_executor_parser.add_argument( + "--executor", required=True, help="Type: address" + ) + set_trusted_executor_parser.add_argument( + "--approved", required=True, help="Type: bool", type=boolean_argument_type + ) + set_trusted_executor_parser.set_defaults(func=handle_set_trusted_executor) + start_at_bat_parser = subcommands.add_parser("start-at-bat") add_default_arguments(start_at_bat_parser, True) start_at_bat_parser.add_argument( @@ -1237,6 +1396,25 @@ def generate_cli() -> argparse.ArgumentParser: ) start_session_parser.set_defaults(func=handle_start_session) + submit_trusted_at_bat_parser = subcommands.add_parser("submit-trusted-at-bat") + add_default_arguments(submit_trusted_at_bat_parser, True) + submit_trusted_at_bat_parser.add_argument( + "--pitcher-nft", required=True, help="Type: tuple", type=eval + ) + submit_trusted_at_bat_parser.add_argument( + "--batter-nft", required=True, help="Type: tuple", type=eval + ) + submit_trusted_at_bat_parser.add_argument( + "--pitches", required=True, help="Type: tuple[]", nargs="+" + ) + submit_trusted_at_bat_parser.add_argument( + "--swings", required=True, help="Type: tuple[]", nargs="+" + ) + submit_trusted_at_bat_parser.add_argument( + "--proposed-outcome", required=True, help="Type: uint8", type=int + ) + submit_trusted_at_bat_parser.set_defaults(func=handle_submit_trusted_at_bat) + swing_hash_parser = subcommands.add_parser("swing-hash") add_default_arguments(swing_hash_parser, False) swing_hash_parser.add_argument( @@ -1253,6 +1431,22 @@ def generate_cli() -> argparse.ArgumentParser: ) swing_hash_parser.set_defaults(func=handle_swing_hash) + trusted_at_bat_hash_parser = subcommands.add_parser("trusted-at-bat-hash") + add_default_arguments(trusted_at_bat_hash_parser, False) + trusted_at_bat_hash_parser.add_argument( + "--at-bat-id", required=True, help="Type: string", type=str + ) + trusted_at_bat_hash_parser.add_argument( + "--nft-address", required=True, help="Type: address" + ) + trusted_at_bat_hash_parser.add_argument( + "--token-id", required=True, help="Type: uint256", type=int + ) + trusted_at_bat_hash_parser.add_argument( + "--role", required=True, help="Type: uint8", type=int + ) + trusted_at_bat_hash_parser.set_defaults(func=handle_trusted_at_bat_hash) + unstake_nft_parser = subcommands.add_parser("unstake-nft") add_default_arguments(unstake_nft_parser, True) unstake_nft_parser.add_argument( @@ -1263,6 +1457,29 @@ def generate_cli() -> argparse.ArgumentParser: ) unstake_nft_parser.set_defaults(func=handle_unstake_nft) + verify_trusted_at_bat_signature_parser = subcommands.add_parser( + "verify-trusted-at-bat-signature" + ) + add_default_arguments(verify_trusted_at_bat_signature_parser, False) + verify_trusted_at_bat_signature_parser.add_argument( + "--at-bat-id", required=True, help="Type: string", type=str + ) + verify_trusted_at_bat_signature_parser.add_argument( + "--nft-address", required=True, help="Type: address" + ) + verify_trusted_at_bat_signature_parser.add_argument( + "--token-id", required=True, help="Type: uint256", type=int + ) + verify_trusted_at_bat_signature_parser.add_argument( + "--role", required=True, help="Type: uint8", type=int + ) + verify_trusted_at_bat_signature_parser.add_argument( + "--signature", required=True, help="Type: bytes", type=bytes_argument_type + ) + verify_trusted_at_bat_signature_parser.set_defaults( + func=handle_verify_trusted_at_bat_signature + ) + return parser diff --git a/src/Fullcount.sol b/src/Fullcount.sol index 0c10f354..471675dd 100644 --- a/src/Fullcount.sol +++ b/src/Fullcount.sol @@ -7,17 +7,18 @@ import { IERC721 } from "../lib/openzeppelin-contracts/contracts/token/ERC721/IE import { SignatureChecker } from "../lib/openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sol"; import { - PlayerType, - PitchSpeed, - SwingType, - VerticalLocation, - HorizontalLocation, - Session, AtBat, + AtBatOutcome, + HorizontalLocation, + NFT, + Outcome, Pitch, + PitchSpeed, + PlayerType, + Session, Swing, - Outcome, - AtBatOutcome + SwingType, + VerticalLocation } from "./data.sol"; /* @@ -56,7 +57,7 @@ Functionality: second commit, then the session is cancelled and both players may unstake their NFTs. */ contract Fullcount is EIP712 { - string public constant FullcountVersion = "0.1.1"; + string public constant FullcountVersion = "0.2.0"; uint256 public SecondsPerPhase; @@ -95,37 +96,47 @@ contract Fullcount is EIP712 { mapping(uint256 => uint256[]) public AtBatSessions; mapping(uint256 => uint256) public SessionAtBat; - event FullcountDeployed(string indexed version, uint256 SecondsPerPhase); - event SessionStarted( - uint256 indexed sessionID, address indexed nftAddress, uint256 indexed tokenID, PlayerType role - ); - event SessionJoined( - uint256 indexed sessionID, address indexed nftAddress, uint256 indexed tokenID, PlayerType role - ); - event SessionExited(uint256 indexed sessionID, address indexed nftAddress, uint256 indexed tokenID); - event SessionAborted(uint256 indexed sessionID, address indexed nftAddress, uint256 indexed tokenID); + // Player address => executor address => bool. Whether or not the + // executor is able to submit at-bats on behalf of the player. + mapping(address => mapping(address => bool)) public TrustedExecutors; - event AtBatStarted( + event AtBatJoined( uint256 indexed atBatID, address indexed nftAddress, uint256 indexed tokenID, uint256 firstSessionID, - PlayerType role, - bool requiresSignature + PlayerType role ); - - event AtBatJoined( + event AtBatProgress( + uint256 indexed atBatID, + AtBatOutcome indexed outcome, + uint256 balls, + uint256 strikes, + address pitcherAddress, + uint256 pitcherTokenID, + address batterAddress, + uint256 batterTokenID + ); + event AtBatStarted( uint256 indexed atBatID, address indexed nftAddress, uint256 indexed tokenID, uint256 firstSessionID, - PlayerType role + PlayerType role, + bool requiresSignature ); - + event ExecutorChange(address indexed player, address indexed executor, bool approved); + event FullcountDeployed(string indexed version, uint256 SecondsPerPhase); event PitchCommitted(uint256 indexed sessionID); - event SwingCommitted(uint256 indexed sessionID); event PitchRevealed(uint256 indexed sessionID, Pitch pitch); - event SwingRevealed(uint256 indexed sessionID, Swing swing); + event SessionAborted(uint256 indexed sessionID, address indexed nftAddress, uint256 indexed tokenID); + event SessionExited(uint256 indexed sessionID, address indexed nftAddress, uint256 indexed tokenID); + event SessionJoined( + uint256 indexed sessionID, address indexed nftAddress, uint256 indexed tokenID, PlayerType role + ); + event SessionStarted( + uint256 indexed sessionID, address indexed nftAddress, uint256 indexed tokenID, PlayerType role + ); event SessionResolved( uint256 indexed sessionID, Outcome indexed outcome, @@ -134,17 +145,8 @@ contract Fullcount is EIP712 { address batterAddress, uint256 batterTokenID ); - - event AtBatProgress( - uint256 indexed atBatID, - AtBatOutcome indexed outcome, - uint256 balls, - uint256 strikes, - address pitcherAddress, - uint256 pitcherTokenID, - address batterAddress, - uint256 batterTokenID - ); + event SwingCommitted(uint256 indexed sessionID); + event SwingRevealed(uint256 indexed sessionID, Swing swing); constructor(uint256 secondsPerPhase) EIP712("Fullcount", FullcountVersion) { SecondsPerPhase = secondsPerPhase; @@ -164,6 +166,19 @@ contract Fullcount is EIP712 { return AtBatSessions[atBatID].length; } + function _isExecutorForPlayer(address executor, address player) internal view returns (bool) { + return TrustedExecutors[player][executor]; + } + + function isExecutorForPlayer(address executor, address player) external view returns (bool) { + return _isExecutorForPlayer(executor, player); + } + + function setTrustedExecutor(address executor, bool approved) external virtual { + TrustedExecutors[msg.sender][executor] = approved; + emit ExecutorChange(msg.sender, executor, approved); + } + /** * Return values: * 0 - session does not exist @@ -387,7 +402,7 @@ contract Fullcount is EIP712 { return NumAtBats; } - function _progressAtBat(uint256 finishedSessionID) internal { + function _progressAtBat(uint256 finishedSessionID, bool updateStakedTokens) internal { uint256 atBatID = SessionAtBat[finishedSessionID]; if (atBatID == 0) return; @@ -400,19 +415,36 @@ contract Fullcount is EIP712 { atBat.outcome = AtBatOutcome.Strikeout; } else { atBat.strikes++; - _startNextAtBatSession( - atBatID, - finishedSession.pitcherNFT.nftAddress, - finishedSession.pitcherNFT.tokenID, - finishedSession.batterNFT.nftAddress, - finishedSession.batterNFT.tokenID - ); + if (updateStakedTokens) { + _startNextAtBatSession( + atBatID, + finishedSession.pitcherNFT.nftAddress, + finishedSession.pitcherNFT.tokenID, + finishedSession.batterNFT.nftAddress, + finishedSession.batterNFT.tokenID + ); + } } } else if (finishedSession.outcome == Outcome.Ball) { if (atBat.balls >= 3) { atBat.outcome = AtBatOutcome.Walk; } else { atBat.balls++; + if (updateStakedTokens) { + _startNextAtBatSession( + atBatID, + finishedSession.pitcherNFT.nftAddress, + finishedSession.pitcherNFT.tokenID, + finishedSession.batterNFT.nftAddress, + finishedSession.batterNFT.tokenID + ); + } + } + } else if (finishedSession.outcome == Outcome.Foul) { + if (atBat.strikes < 2) { + atBat.strikes++; + } + if (updateStakedTokens) { _startNextAtBatSession( atBatID, finishedSession.pitcherNFT.nftAddress, @@ -421,17 +453,6 @@ contract Fullcount is EIP712 { finishedSession.batterNFT.tokenID ); } - } else if (finishedSession.outcome == Outcome.Foul) { - if (atBat.strikes < 2) { - atBat.strikes++; - } - _startNextAtBatSession( - atBatID, - finishedSession.pitcherNFT.nftAddress, - finishedSession.pitcherNFT.tokenID, - finishedSession.batterNFT.nftAddress, - finishedSession.batterNFT.tokenID - ); } else if (finishedSession.outcome == Outcome.Single) { atBat.outcome = AtBatOutcome.Single; } else if (finishedSession.outcome == Outcome.Double) { @@ -826,7 +847,7 @@ contract Fullcount is EIP712 { StakedSession[session.pitcherNFT.nftAddress][session.pitcherNFT.tokenID] = 0; session.pitcherLeftSession = true; - _progressAtBat(sessionID); + _progressAtBat(sessionID, true); } } @@ -883,7 +904,122 @@ contract Fullcount is EIP712 { StakedSession[session.pitcherNFT.nftAddress][session.pitcherNFT.tokenID] = 0; session.pitcherLeftSession = true; - _progressAtBat(sessionID); + _progressAtBat(sessionID, true); + } + } + + // *** Trusted Execution *** + function trustedAtBatHash( + string memory atBatID, + address nftAddress, + uint256 tokenID, + PlayerType role + ) + public + view + returns (bytes32) + { + bytes32 structHash = keccak256( + abi.encode( + keccak256("TrustedAtBatMessage(string atBatID,address nftAddress,uint256 tokenID,uint256 role)"), + atBatID, + nftAddress, + tokenID, + uint256(role) + ) + ); + return _hashTypedDataV4(structHash); + } + + function verifyTrustedAtBatSignature( + string memory atBatID, + address nftAddress, + uint256 tokenID, + PlayerType role, + bytes memory signature + ) + external + view + returns (bool) + { + address owner = IERC721(nftAddress).ownerOf(tokenID); + bytes32 messageHash = trustedAtBatHash(atBatID, nftAddress, tokenID, role); + return SignatureChecker.isValidSignatureNow(owner, messageHash, signature); + } + + function submitTrustedAtBat( + NFT memory pitcherNFT, + NFT memory batterNFT, + Pitch[] memory pitches, + Swing[] memory swings, + AtBatOutcome proposedOutcome + ) + external + { + require( + pitches.length == swings.length, + "Fullcount.submitTrustedAtBat: number of pitches does not match number of swings." + ); + + address pitcherOwner = IERC721(pitcherNFT.nftAddress).ownerOf(pitcherNFT.tokenID); + require( + _isExecutorForPlayer(msg.sender, pitcherOwner), + "Fullcount.submitTrustedAtBat: sender is not an executor for pitcher." + ); + + address batterOwner = IERC721(batterNFT.nftAddress).ownerOf(batterNFT.tokenID); + require( + _isExecutorForPlayer(msg.sender, batterOwner), + "Fullcount.submitTrustedAtBat: sender is not an executor for batter." + ); + + // Create at-bat + NumAtBats++; + + AtBatState[NumAtBats].pitcherNFT = pitcherNFT; + AtBatState[NumAtBats].batterNFT = batterNFT; + + uint256[] storage sessionList = AtBatSessions[NumAtBats]; + + for (uint256 i = 0; i < pitches.length; i++) { + if (AtBatState[NumAtBats].outcome != AtBatOutcome.InProgress) { + revert("Fullcount.submitTrustedAtBat: invalid at-bat - invalid at-bat"); + } + + Outcome sessionOutcome = resolve(pitches[i], swings[i]); + + NumSessions++; + SessionState[NumSessions].pitcherNFT = pitcherNFT; + SessionState[NumSessions].batterNFT = batterNFT; + SessionState[NumSessions].outcome = sessionOutcome; + + // Add session to at-bat + sessionList.push(NumSessions); + SessionAtBat[NumSessions] = NumAtBats; + + emit PitchCommitted(NumSessions); + emit SwingCommitted(NumSessions); + emit PitchRevealed(NumSessions, pitches[i]); + emit SwingRevealed(NumSessions, swings[i]); + + emit SessionResolved( + NumSessions, + sessionOutcome, + pitcherNFT.nftAddress, + pitcherNFT.tokenID, + batterNFT.nftAddress, + batterNFT.tokenID + ); + + _progressAtBat(NumSessions, false); + } + + if (AtBatState[NumAtBats].outcome == AtBatOutcome.InProgress) { + revert("Fullcount.submitTrustedAtBat: invalid at-bat - inconclusive"); + } + + if (AtBatState[NumAtBats].outcome != proposedOutcome) { + revert("Fullcount.submitTrustedAtBat: at-bat outcome does not match executor proposed outcome"); } } } diff --git a/test/AtBats.t.sol b/test/AtBats.t.sol index 8d94b261..c107c119 100644 --- a/test/AtBats.t.sol +++ b/test/AtBats.t.sol @@ -44,6 +44,61 @@ contract FullcountAtBatTest is FullcountTestBase { address batterAddress, uint256 batterTokenID ); + + function _generateStrike() internal pure returns (Pitch memory, Swing memory) { + Pitch memory pitch = Pitch(1, PitchSpeed.Fast, VerticalLocation.Middle, HorizontalLocation.InsideStrike); + + Swing memory swing = Swing(1, SwingType.Take, VerticalLocation.Middle, HorizontalLocation.Middle); + + return (pitch, swing); + } + + function _generateBall() internal pure returns (Pitch memory, Swing memory) { + Pitch memory pitch = Pitch(1, PitchSpeed.Fast, VerticalLocation.Middle, HorizontalLocation.InsideBall); + + Swing memory swing = Swing(1, SwingType.Take, VerticalLocation.Middle, HorizontalLocation.Middle); + + return (pitch, swing); + } + + function _generateFoul() internal pure returns (Pitch memory, Swing memory) { + Pitch memory pitch = Pitch( + 76_272_677_889_733_487_807_869_088_975_394_561_199_007_238_211_299_295_369_669_345_782_657_832_457_462, + PitchSpeed.Slow, + VerticalLocation.HighBall, + HorizontalLocation.OutsideStrike + ); + + Swing memory swing = Swing(5027, SwingType.Contact, VerticalLocation.HighBall, HorizontalLocation.OutsideStrike); + + return (pitch, swing); + } + + function _generateDouble() internal pure returns (Pitch memory, Swing memory) { + Pitch memory pitch = Pitch( + 111_226_050_657_822_924_597_491_446_253_991_213_025_840_145_394_201_015_488_938_793_738_637_304_727_056, + PitchSpeed.Slow, + VerticalLocation.Middle, + HorizontalLocation.InsideStrike + ); + + Swing memory swing = Swing(5682, SwingType.Power, VerticalLocation.Middle, HorizontalLocation.OutsideStrike); + + return (pitch, swing); + } + + function _generateHomeRun() internal pure returns (Pitch memory, Swing memory) { + Pitch memory pitch = Pitch( + 70_742_784_270_056_657_581_884_307_797_108_841_089_344_138_257_779_225_355_304_684_713_507_588_495_343, + PitchSpeed.Fast, + VerticalLocation.HighBall, + HorizontalLocation.OutsideStrike + ); + + Swing memory swing = Swing(6874, SwingType.Power, VerticalLocation.HighBall, HorizontalLocation.OutsideStrike); + + return (pitch, swing); + } } contract FullcountTest_startAtBat is FullcountAtBatTest { @@ -259,44 +314,33 @@ contract FullcountTest_ballsAndStrikes is FullcountAtBatTest { } function _strikeSession(uint256 sessionID) internal { - Pitch memory pitch = Pitch(1, PitchSpeed.Fast, VerticalLocation.Middle, HorizontalLocation.InsideStrike); - _commitPitch(sessionID, player1, player1PrivateKey, pitch); + (Pitch memory strikePitch, Swing memory strikeSwing) = _generateStrike(); - Swing memory swing = Swing(1, SwingType.Take, VerticalLocation.Middle, HorizontalLocation.Middle); - _commitSwing(sessionID, player2, player2PrivateKey, swing); + _commitPitch(sessionID, player1, player1PrivateKey, strikePitch); + _commitSwing(sessionID, player2, player2PrivateKey, strikeSwing); - _revealPitch(sessionID, player1, pitch); - _revealSwing(sessionID, player2, swing); + _revealPitch(sessionID, player1, strikePitch); + _revealSwing(sessionID, player2, strikeSwing); } function _ballSession(uint256 sessionID) internal { - Pitch memory pitch = Pitch(1, PitchSpeed.Fast, VerticalLocation.Middle, HorizontalLocation.InsideBall); - _commitPitch(sessionID, player1, player1PrivateKey, pitch); + (Pitch memory ballPitch, Swing memory ballSwing) = _generateBall(); - Swing memory swing = Swing(1, SwingType.Take, VerticalLocation.Middle, HorizontalLocation.Middle); - _commitSwing(sessionID, player2, player2PrivateKey, swing); + _commitPitch(sessionID, player1, player1PrivateKey, ballPitch); + _commitSwing(sessionID, player2, player2PrivateKey, ballSwing); - _revealSwing(sessionID, player2, swing); - _revealPitch(sessionID, player1, pitch); + _revealSwing(sessionID, player2, ballSwing); + _revealPitch(sessionID, player1, ballPitch); } function _foulSession(uint256 sessionID) internal { - // From Resolutinos test file + (Pitch memory foulPitch, Swing memory foulSwing) = _generateFoul(); - Pitch memory pitch = Pitch( - 76_272_677_889_733_487_807_869_088_975_394_561_199_007_238_211_299_295_369_669_345_782_657_832_457_462, - PitchSpeed.Slow, - VerticalLocation.HighBall, - HorizontalLocation.OutsideStrike - ); + _commitPitch(sessionID, player1, player1PrivateKey, foulPitch); + _commitSwing(sessionID, player2, player2PrivateKey, foulSwing); - Swing memory swing = Swing(5027, SwingType.Contact, VerticalLocation.HighBall, HorizontalLocation.OutsideStrike); - - _commitPitch(sessionID, player1, player1PrivateKey, pitch); - _commitSwing(sessionID, player2, player2PrivateKey, swing); - - _revealPitch(sessionID, player1, pitch); - _revealSwing(sessionID, player2, swing); + _revealPitch(sessionID, player1, foulPitch); + _revealSwing(sessionID, player2, foulSwing); } function test_new_session_begins_after_one_strike() public { @@ -795,21 +839,13 @@ contract FullcountTest_ballsAndStrikes is FullcountAtBatTest { assertEq(atBat.strikes, 2); assertEq(uint256(atBat.outcome), uint256(AtBatOutcome.InProgress)); - // Generates a double - Pitch memory pitch = Pitch( - 111_226_050_657_822_924_597_491_446_253_991_213_025_840_145_394_201_015_488_938_793_738_637_304_727_056, - PitchSpeed.Slow, - VerticalLocation.Middle, - HorizontalLocation.InsideStrike - ); + (Pitch memory doublePitch, Swing memory doubleSwing) = _generateDouble(); - Swing memory swing = Swing(5682, SwingType.Power, VerticalLocation.Middle, HorizontalLocation.OutsideStrike); - - _commitPitch(sixthSessionID, player1, player1PrivateKey, pitch); - _commitSwing(sixthSessionID, player2, player2PrivateKey, swing); + _commitPitch(sixthSessionID, player1, player1PrivateKey, doublePitch); + _commitSwing(sixthSessionID, player2, player2PrivateKey, doubleSwing); - _revealPitch(sixthSessionID, player1, pitch); - _revealSwing(sixthSessionID, player2, swing); + _revealPitch(sixthSessionID, player1, doublePitch); + _revealSwing(sixthSessionID, player2, doubleSwing); Session memory sixthSession = game.getSession(sixthSessionID); assertTrue(sixthSession.didPitcherReveal); @@ -1029,15 +1065,7 @@ contract FullcountTest_ballsAndStrikes is FullcountAtBatTest { nextSessionID = game.AtBatSessions(AtBatID, 5); - // Generates a home run - Pitch memory hrPitch = Pitch( - 70_742_784_270_056_657_581_884_307_797_108_841_089_344_138_257_779_225_355_304_684_713_507_588_495_343, - PitchSpeed.Fast, - VerticalLocation.HighBall, - HorizontalLocation.OutsideStrike - ); - - Swing memory hrSwing = Swing(6874, SwingType.Power, VerticalLocation.HighBall, HorizontalLocation.OutsideStrike); + (Pitch memory hrPitch, Swing memory hrSwing) = _generateHomeRun(); _commitPitch(nextSessionID, player1, player1PrivateKey, hrPitch); diff --git a/test/Fullcount.t.sol b/test/Fullcount.t.sol index c378d231..5acac8a1 100644 --- a/test/Fullcount.t.sol +++ b/test/Fullcount.t.sol @@ -207,9 +207,9 @@ contract FullcountTestBase is Test { contract FullcountTestDeployment is FullcountTestBase { function test_Deployment() public { vm.expectEmit(); - emit FullcountDeployed("0.1.1", secondsPerPhase); + emit FullcountDeployed("0.2.0", secondsPerPhase); Fullcount newGame = new Fullcount(secondsPerPhase); - assertEq(newGame.FullcountVersion(), "0.1.1"); + assertEq(newGame.FullcountVersion(), "0.2.0"); assertEq(newGame.SecondsPerPhase(), secondsPerPhase); assertEq(newGame.NumSessions(), 0); } diff --git a/test/Resolution.t.sol b/test/Resolution.t.sol index 617c9b9f..36518267 100644 --- a/test/Resolution.t.sol +++ b/test/Resolution.t.sol @@ -1280,4 +1280,46 @@ contract ResolutionTest is FullcountTestBase { assertEq(game.sessionProgress(SessionID), 5); } + + function test_trusted_exec_example() public { + assertEq(game.sessionProgress(SessionID), 3); + + Pitch memory pitch = Pitch(125, PitchSpeed.Slow, VerticalLocation.Middle, HorizontalLocation.OutsideStrike); + + _commitPitch(SessionID, player1, player1PrivateKey, pitch); + + assertEq(game.sessionProgress(SessionID), 3); + + Swing memory swing = Swing(126, SwingType.Power, VerticalLocation.Middle, HorizontalLocation.OutsideStrike); + + _commitSwing(SessionID, player2, player2PrivateKey, swing); + + assertEq(game.sessionProgress(SessionID), 4); + + // Pitcher reveals first. + _revealPitch(SessionID, player1, pitch); + + vm.startPrank(player2); + + vm.expectEmit(address(game)); + emit SessionResolved( + SessionID, Outcome.Triple, PitcherNFTAddress, PitcherTokenID, BatterNFTAddress, BatterTokenID + ); + + game.revealSwing(SessionID, swing.nonce, swing.kind, swing.vertical, swing.horizontal); + + vm.stopPrank(); + + Session memory session = game.getSession(SessionID); + assertTrue(session.didBatterReveal); + assertEq(uint256(session.outcome), uint256(Outcome.Triple)); + + Swing memory sessionSwing = session.batterReveal; + assertEq(sessionSwing.nonce, swing.nonce); + assertEq(uint256(sessionSwing.kind), uint256(swing.kind)); + assertEq(uint256(sessionSwing.vertical), uint256(swing.vertical)); + assertEq(uint256(sessionSwing.horizontal), uint256(swing.horizontal)); + + assertEq(game.sessionProgress(SessionID), 5); + } } diff --git a/test/TrustedExecution.t.sol b/test/TrustedExecution.t.sol new file mode 100644 index 00000000..1f38bda6 --- /dev/null +++ b/test/TrustedExecution.t.sol @@ -0,0 +1,568 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import { Test, console2 } from "../lib/forge-std/src/Test.sol"; +import { FullcountAtBatTest } from "./AtBats.t.sol"; +import { + AtBat, + AtBatOutcome, + HorizontalLocation, + NFT, + Outcome, + Pitch, + PitchSpeed, + PlayerType, + Session, + Swing, + SwingType, + VerticalLocation +} from "../src/data.sol"; + +contract TrustedExecutionTest is FullcountAtBatTest { + uint256 executor1PrivateKey = 0x101; + address executor1 = vm.addr(executor1PrivateKey); + + event ExecutorChange(address indexed player, address indexed executor, bool approved); + + function _setExecutorForPlayer(address executor, address player, bool approved) internal { + vm.prank(player); + game.setTrustedExecutor(executor, approved); + } +} + +contract TrustedExecutionTest_executors is TrustedExecutionTest { + function test_set_executor() public { + assertFalse(game.isExecutorForPlayer(executor1, player1)); + assertFalse(game.isExecutorForPlayer(randomPerson, player1)); + + vm.expectEmit(address(game)); + emit ExecutorChange(player1, executor1, true); + _setExecutorForPlayer(executor1, player1, true); + + assertTrue(game.isExecutorForPlayer(executor1, player1)); + assertFalse(game.isExecutorForPlayer(randomPerson, player1)); + } +} + +contract TrustedExecutionTest_signature is TrustedExecutionTest { + uint256 PitcherTokenID; + uint256 BatterTokenID; + uint256 MaliciousPlayerTokenID; + + function setUp() public virtual override { + super.setUp(); + + charactersMinted++; + uint256 tokenID = charactersMinted; + + otherCharactersMinted++; + uint256 otherTokenID = otherCharactersMinted; + + characterNFTs.mint(player1, tokenID); + otherCharacterNFTs.mint(player2, otherTokenID); + + PitcherTokenID = tokenID; + BatterTokenID = otherTokenID; + + charactersMinted++; + MaliciousPlayerTokenID = charactersMinted; + characterNFTs.mint(randomPerson, MaliciousPlayerTokenID); + } + + function test_nft_owner_is_verified() public { + string memory atBatID = "d5bb2e54-9a17-4d22-8813-04bbfe738d61"; + + bytes32 pitcherTrustedMessageHash = + game.trustedAtBatHash(atBatID, address(characterNFTs), PitcherTokenID, PlayerType.Pitcher); + bytes memory pitcherSignature = signMessageHash(player1PrivateKey, pitcherTrustedMessageHash); + + assertTrue( + game.verifyTrustedAtBatSignature( + atBatID, address(characterNFTs), PitcherTokenID, PlayerType.Pitcher, pitcherSignature + ) + ); + + bytes32 batterTrustedMessageHash = + game.trustedAtBatHash(atBatID, address(otherCharacterNFTs), BatterTokenID, PlayerType.Batter); + bytes memory batterSignature = signMessageHash(player2PrivateKey, batterTrustedMessageHash); + + assertTrue( + game.verifyTrustedAtBatSignature( + atBatID, address(otherCharacterNFTs), BatterTokenID, PlayerType.Batter, batterSignature + ) + ); + } + + function test_non_owner_is_not_verified() public { + string memory atBatID = "d5bb2e54-9a17-4d22-8813-04bbfe738d61"; + string memory anotherAtBatID = "f4249298-335b-4976-a6f1-6226eb4382ac"; + + bytes32 firstAtBatMessageHash = + game.trustedAtBatHash(atBatID, address(characterNFTs), PitcherTokenID, PlayerType.Pitcher); + // Malicious player signs message for token he does not own. + bytes memory wrongTokenSignature = signMessageHash(randomPersonPrivateKey, firstAtBatMessageHash); + + assertFalse( + game.verifyTrustedAtBatSignature( + atBatID, address(characterNFTs), PitcherTokenID, PlayerType.Pitcher, wrongTokenSignature + ) + ); + + bytes32 secondAtBatMessageHash = + game.trustedAtBatHash(anotherAtBatID, address(characterNFTs), MaliciousPlayerTokenID, PlayerType.Pitcher); + // Malicious player signs a message for another at-bat. + bytes memory anotherAtBatSignature = signMessageHash(randomPersonPrivateKey, secondAtBatMessageHash); + + // Malicous player's signature is valid for second at-bat. + assertTrue( + game.verifyTrustedAtBatSignature( + anotherAtBatID, + address(characterNFTs), + MaliciousPlayerTokenID, + PlayerType.Pitcher, + anotherAtBatSignature + ) + ); + + // But he cannot sign use that signature for another at-bat + assertFalse( + game.verifyTrustedAtBatSignature( + atBatID, address(characterNFTs), MaliciousPlayerTokenID, PlayerType.Pitcher, anotherAtBatSignature + ) + ); + } +} + +contract TrustedExecutionTest_submitTrustedAtBat is TrustedExecutionTest { + uint256 PitcherTokenID; + uint256 BatterTokenID; + + function setUp() public virtual override { + super.setUp(); + + charactersMinted++; + uint256 tokenID = charactersMinted; + + otherCharactersMinted++; + uint256 otherTokenID = otherCharactersMinted; + + characterNFTs.mint(player1, tokenID); + otherCharacterNFTs.mint(player2, otherTokenID); + + PitcherTokenID = tokenID; + BatterTokenID = otherTokenID; + } + + function test_must_be_executor_for_both_players_to_submit_at_bat() public { + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + + Pitch[] memory pitches = new Pitch[](1); + pitches[0] = Pitch({ + nonce: 0, + speed: PitchSpeed.Fast, + vertical: VerticalLocation.Middle, + horizontal: HorizontalLocation.Middle + }); + + Swing[] memory swings = new Swing[](1); + swings[0] = Swing({ + nonce: 0, + kind: SwingType.Contact, + vertical: VerticalLocation.Middle, + horizontal: HorizontalLocation.Middle + }); + + vm.prank(executor1); + vm.expectRevert("Fullcount.submitTrustedAtBat: sender is not an executor for pitcher."); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.HomeRun); + + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(executor1); + vm.expectRevert("Fullcount.submitTrustedAtBat: sender is not an executor for batter."); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.HomeRun); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + vm.prank(executor1); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.HomeRun); + } + + function test_submit_at_bat_populates_at_bat_and_sessions() public { + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + uint256 initialNumAtBats = game.NumAtBats(); + uint256 initialNumSessions = game.NumSessions(); + + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + Pitch[] memory pitches = new Pitch[](1); + Swing[] memory swings = new Swing[](1); + + (pitches[0], swings[0]) = _generateHomeRun(); + + vm.prank(executor1); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.HomeRun); + + uint256 atBatID = game.NumAtBats(); + + assertEq(atBatID, initialNumAtBats + 1); + assertEq(game.NumSessions(), initialNumSessions + 1); + + AtBat memory atBat = game.getAtBat(atBatID); + assertEq(atBat.pitcherNFT.nftAddress, pitcher.nftAddress); + assertEq(atBat.pitcherNFT.tokenID, pitcher.tokenID); + assertEq(atBat.batterNFT.nftAddress, batter.nftAddress); + assertEq(atBat.batterNFT.tokenID, batter.tokenID); + assertTrue(atBat.outcome == AtBatOutcome.HomeRun); + + Session memory session = game.getSession(game.AtBatSessions(atBatID, 0)); + assertEq(session.pitcherNFT.nftAddress, pitcher.nftAddress); + assertEq(session.pitcherNFT.tokenID, pitcher.tokenID); + assertEq(session.batterNFT.nftAddress, batter.nftAddress); + assertEq(session.batterNFT.tokenID, batter.tokenID); + assertTrue(session.outcome == Outcome.HomeRun); + } + + function test_submit_three_pitch_strikeout() public { + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + uint256 initialNumAtBats = game.NumAtBats(); + uint256 initialNumSessions = game.NumSessions(); + + uint256 atBatLength = 3; + + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + Pitch[] memory pitches = new Pitch[](atBatLength); + Swing[] memory swings = new Swing[](atBatLength); + + (pitches[0], swings[0]) = _generateStrike(); + (pitches[1], swings[1]) = _generateStrike(); + (pitches[2], swings[2]) = _generateStrike(); + + vm.prank(executor1); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.Strikeout); + + uint256 atBatID = game.NumAtBats(); + + assertEq(atBatID, initialNumAtBats + 1); + assertEq(game.NumSessions(), initialNumSessions + atBatLength); + + AtBat memory atBat = game.getAtBat(atBatID); + assertEq(atBat.balls, 0); + assertEq(atBat.strikes, 2); + assertTrue(atBat.outcome == AtBatOutcome.Strikeout); + + Session memory session; + for (uint256 i = 0; i < atBatLength; i++) { + session = game.getSession(game.AtBatSessions(atBatID, i)); + assertTrue(session.outcome == Outcome.Strike); + } + } + + function test_submit_four_ball_walk() public { + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + uint256 initialNumAtBats = game.NumAtBats(); + uint256 initialNumSessions = game.NumSessions(); + + uint256 atBatLength = 4; + + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + Pitch[] memory pitches = new Pitch[](atBatLength); + Swing[] memory swings = new Swing[](atBatLength); + + (pitches[0], swings[0]) = _generateBall(); + (pitches[1], swings[1]) = _generateBall(); + (pitches[2], swings[2]) = _generateBall(); + (pitches[3], swings[3]) = _generateBall(); + + vm.prank(executor1); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.Walk); + + uint256 atBatID = game.NumAtBats(); + + assertEq(atBatID, initialNumAtBats + 1); + assertEq(game.NumSessions(), initialNumSessions + atBatLength); + + AtBat memory atBat = game.getAtBat(atBatID); + assertEq(atBat.balls, 3); + assertEq(atBat.strikes, 0); + assertTrue(atBat.outcome == AtBatOutcome.Walk); + + Session memory session; + for (uint256 i = 0; i < atBatLength; i++) { + session = game.getSession(game.AtBatSessions(atBatID, i)); + assertTrue(session.outcome == Outcome.Ball); + } + } + + function test_submit_strike_strike_foul_double() public { + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + uint256 initialNumAtBats = game.NumAtBats(); + uint256 initialNumSessions = game.NumSessions(); + + uint256 atBatLength = 4; + + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + Pitch[] memory pitches = new Pitch[](atBatLength); + Swing[] memory swings = new Swing[](atBatLength); + + (pitches[0], swings[0]) = _generateStrike(); + (pitches[1], swings[1]) = _generateStrike(); + (pitches[2], swings[2]) = _generateFoul(); + (pitches[3], swings[3]) = _generateDouble(); + + vm.prank(executor1); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.Double); + + uint256 atBatID = game.NumAtBats(); + + assertEq(atBatID, initialNumAtBats + 1); + assertEq(game.NumSessions(), initialNumSessions + atBatLength); + + AtBat memory atBat = game.getAtBat(atBatID); + assertEq(atBat.balls, 0); + assertEq(atBat.strikes, 2); + assertTrue(atBat.outcome == AtBatOutcome.Double); + + Session memory session; + for (uint256 i = 0; i < atBatLength; i++) { + session = game.getSession(game.AtBatSessions(atBatID, i)); + if (i == 0 || i == 1) { + assertTrue(session.outcome == Outcome.Strike); + } else if (i == 2) { + assertTrue(session.outcome == Outcome.Foul); + } else { + assertTrue(session.outcome == Outcome.Double); + } + } + } + + function test_submit_ball_then_home_run_events() public { + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + uint256 initialNumAtBats = game.NumAtBats(); + uint256 initialNumSessions = game.NumSessions(); + + uint256 atBatLength = 2; + + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + Pitch[] memory pitches = new Pitch[](atBatLength); + Swing[] memory swings = new Swing[](atBatLength); + + (pitches[0], swings[0]) = _generateBall(); + (pitches[1], swings[1]) = _generateHomeRun(); + + vm.prank(executor1); + vm.expectEmit(address(game)); + emit PitchCommitted(initialNumSessions + 1); + vm.expectEmit(address(game)); + emit SwingCommitted(initialNumSessions + 1); + vm.expectEmit(address(game)); + emit PitchRevealed(initialNumSessions + 1, pitches[0]); + vm.expectEmit(address(game)); + emit SwingRevealed(initialNumSessions + 1, swings[0]); + vm.expectEmit(address(game)); + emit SessionResolved( + initialNumSessions + 1, + Outcome.Ball, + address(characterNFTs), + PitcherTokenID, + address(otherCharacterNFTs), + BatterTokenID + ); + vm.expectEmit(address(game)); + emit AtBatProgress( + initialNumAtBats + 1, + AtBatOutcome.InProgress, + 1, + 0, + address(characterNFTs), + PitcherTokenID, + address(otherCharacterNFTs), + BatterTokenID + ); + vm.expectEmit(address(game)); + emit PitchCommitted(initialNumSessions + 2); + vm.expectEmit(address(game)); + emit SwingCommitted(initialNumSessions + 2); + vm.expectEmit(address(game)); + emit PitchRevealed(initialNumSessions + 2, pitches[1]); + vm.expectEmit(address(game)); + emit SwingRevealed(initialNumSessions + 2, swings[1]); + vm.expectEmit(address(game)); + emit SessionResolved( + initialNumSessions + 2, + Outcome.HomeRun, + address(characterNFTs), + PitcherTokenID, + address(otherCharacterNFTs), + BatterTokenID + ); + vm.expectEmit(address(game)); + emit AtBatProgress( + initialNumAtBats + 1, + AtBatOutcome.HomeRun, + 1, + 0, + address(characterNFTs), + PitcherTokenID, + address(otherCharacterNFTs), + BatterTokenID + ); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.HomeRun); + + uint256 atBatID = game.NumAtBats(); + + assertEq(atBatID, initialNumAtBats + 1); + assertEq(game.NumSessions(), initialNumSessions + atBatLength); + + AtBat memory atBat = game.getAtBat(atBatID); + assertEq(atBat.balls, 1); + assertEq(atBat.strikes, 0); + assertTrue(atBat.outcome == AtBatOutcome.HomeRun); + + Session memory session; + for (uint256 i = 0; i < atBatLength; i++) { + session = game.getSession(game.AtBatSessions(atBatID, i)); + if (i == 0) { + assertTrue(session.outcome == Outcome.Ball); + } else { + assertTrue(session.outcome == Outcome.HomeRun); + } + } + } + + function test_submit_with_inconclusive_at_bat() public { + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + uint256 initialNumAtBats = game.NumAtBats(); + uint256 initialNumSessions = game.NumSessions(); + + uint256 atBatLength = 6; + + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + Pitch[] memory pitches = new Pitch[](atBatLength); + Swing[] memory swings = new Swing[](atBatLength); + + (pitches[0], swings[0]) = _generateStrike(); + (pitches[1], swings[1]) = _generateBall(); + (pitches[2], swings[2]) = _generateStrike(); + (pitches[3], swings[3]) = _generateBall(); + (pitches[4], swings[4]) = _generateFoul(); + (pitches[5], swings[5]) = _generateBall(); + + vm.prank(executor1); + vm.expectRevert("Fullcount.submitTrustedAtBat: invalid at-bat - inconclusive"); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.InProgress); + + assertEq(game.NumAtBats(), initialNumAtBats); + assertEq(game.NumSessions(), initialNumSessions); + } + + function test_submit_with_multiple_finalities() public { + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + uint256 initialNumAtBats = game.NumAtBats(); + uint256 initialNumSessions = game.NumSessions(); + + uint256 atBatLength = 4; + + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + Pitch[] memory pitches = new Pitch[](atBatLength); + Swing[] memory swings = new Swing[](atBatLength); + + (pitches[0], swings[0]) = _generateFoul(); + (pitches[1], swings[1]) = _generateStrike(); + (pitches[2], swings[2]) = _generateStrike(); + (pitches[3], swings[3]) = _generateDouble(); + + vm.prank(executor1); + vm.expectRevert("Fullcount.submitTrustedAtBat: invalid at-bat - invalid at-bat"); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.Double); + + assertEq(game.NumAtBats(), initialNumAtBats); + assertEq(game.NumSessions(), initialNumSessions); + } + + function test_at_bat_outcome_must_match_executor_proposed_outcome() public { + vm.prank(player1); + game.setTrustedExecutor(executor1, true); + + vm.prank(player2); + game.setTrustedExecutor(executor1, true); + + uint256 initialNumAtBats = game.NumAtBats(); + uint256 initialNumSessions = game.NumSessions(); + + uint256 atBatLength = 7; + + NFT memory pitcher = NFT({ nftAddress: address(characterNFTs), tokenID: PitcherTokenID }); + NFT memory batter = NFT({ nftAddress: address(otherCharacterNFTs), tokenID: BatterTokenID }); + Pitch[] memory pitches = new Pitch[](atBatLength); + Swing[] memory swings = new Swing[](atBatLength); + + (pitches[0], swings[0]) = _generateFoul(); + (pitches[1], swings[1]) = _generateFoul(); + (pitches[2], swings[2]) = _generateFoul(); + (pitches[3], swings[3]) = _generateBall(); + (pitches[4], swings[4]) = _generateBall(); + (pitches[5], swings[5]) = _generateBall(); + (pitches[6], swings[6]) = _generateBall(); + + vm.prank(executor1); + vm.expectRevert("Fullcount.submitTrustedAtBat: at-bat outcome does not match executor proposed outcome"); + game.submitTrustedAtBat(pitcher, batter, pitches, swings, AtBatOutcome.Strikeout); + + assertEq(game.NumAtBats(), initialNumAtBats); + assertEq(game.NumSessions(), initialNumSessions); + } + + function test_xor_swap() public { + uint256 x = 5; + uint256 y = 17; + x = x ^ (y = y ^ (x = x ^ y)); + assertEq(x, 17); + assertEq(y, 5); + } +}