From 596e8dec08e0e559361b130efc7e85a7b5128d41 Mon Sep 17 00:00:00 2001 From: six <51x@keemail.me> Date: Thu, 6 Oct 2022 17:17:05 +0200 Subject: [PATCH] Receipt reuse and forgery tools --- README.md | 50 ++-- ink/Cargo.toml | 26 +++ ink/lib.rs | 532 +++++++++++++++++++++++++++++++++++++++++++ ink/token_minter.ink | 0 python/forgery.py | 50 ++++ solidity/cctf9v1.sol | 138 +++++++++++ 6 files changed, 771 insertions(+), 25 deletions(-) create mode 100644 ink/Cargo.toml create mode 100644 ink/lib.rs delete mode 100644 ink/token_minter.ink create mode 100644 solidity/cctf9v1.sol diff --git a/README.md b/README.md index cc121e3..93ba18a 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,42 @@ -# pwn_w3bridges +# pwn w3bridges +Workshop for "web3" bridge hacking at Hacktivity 2022 -Workshop for web3 bridge hacking at Hacktivity 2022 +## Agenda +#### Introduction +- Web3 vs web2 hacking, concepts / workshop topology -# Scenario 1 - Receipt reuse - Topology +#### Environment setup, system requirements +- Any browser for Ethereum, Remix +- Substrate, Rust nightly -Story: Substrate system being built after ERC20 token is sold. +#### Scenario 1: Token on two chains, mint using receipt +- Solidity basics, using remix for compile +- Exploit visibility, take admin +- ECDSA Ethereum basics +- Mint with receipt -> Find the vuln! -- Substrate node with EVM pallet || Token minter smart contract (vuln here) -- Bridge providing receipts || Checks bridge balance on Substrate node -- Ethereum node || Token minter smart contract (target for mint) +#### Scenario 2: Signature forgery (any chain) +- Deploy SC on Ethereum chain +- Compile Substrate with EVM +- Deploy SC +- Test ECDSA signature forgery exploit from one to other +## Resources + +#### Scenario 1 https://remix.ethereum.org/ -Faucet? +https://www.tutorialspoint.com/solidity/solidity_operators.htm https://polkadot.js.org/apps/ https://ethereum.org/en/developers/docs/standards/tokens/erc-20/ https://git.hsbp.org/six/eth_keygen +#### Scenario 2 https://github.com/paritytech/substrate-contracts-node https://docs.substrate.io/quick-start/ -https://substrate.io/developers/playground/ | alternative https://github.com/substrate-developer-hub/substrate-front-end-template - - -## Commands -$ - -# Scenario 2 - ECDSA signature forgery - Topology - -Story: ink! smart contract interoperability. - -- Substrate node with ink! -- No bridge, but signature forgery -- Ethereum node - https://github.com/paritytech/ink +https://github.com/paritytech/substrate/blob/master/primitives/core/src/ecdsa.rs https://use.ink/getting-started/setup https://medium.com/block-journal/introducing-substrate-smart-contracts-with-ink-d486289e2b59 - -## Commands -$ +https://substrate.io/developers/playground/ diff --git a/ink/Cargo.toml b/ink/Cargo.toml new file mode 100644 index 0000000..c210b86 --- /dev/null +++ b/ink/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "erc20" +version = "4.0.0-alpha.3" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "erc20" +path = "lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/ink/lib.rs b/ink/lib.rs new file mode 100644 index 0000000..a624e48 --- /dev/null +++ b/ink/lib.rs @@ -0,0 +1,532 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[ink::contract] +mod erc20 { + use ink::storage::Mapping; + + /// A simple ERC-20 contract. + #[ink(storage)] + #[derive(Default)] + pub struct Erc20 { + /// Total token supply. + total_supply: Balance, + /// Mapping from owner to number of owned token. + balances: Mapping, + /// Mapping of the token amount which an account is allowed to withdraw + /// from another account. + allowances: Mapping<(AccountId, AccountId), Balance>, + } + + /// Event emitted when a token transfer occurs. + #[ink(event)] + pub struct Transfer { + #[ink(topic)] + from: Option, + #[ink(topic)] + to: Option, + value: Balance, + } + + /// Event emitted when an approval occurs that `spender` is allowed to withdraw + /// up to the amount of `value` tokens from `owner`. + #[ink(event)] + pub struct Approval { + #[ink(topic)] + owner: AccountId, + #[ink(topic)] + spender: AccountId, + value: Balance, + } + + /// The ERC-20 error types. + #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum Error { + /// Returned if not enough balance to fulfill a request is available. + InsufficientBalance, + /// Returned if not enough allowance to fulfill a request is available. + InsufficientAllowance, + } + + /// The ERC-20 result type. + pub type Result = core::result::Result; + + impl Erc20 { + /// Creates a new ERC-20 contract with the specified initial supply. + #[ink(constructor)] + pub fn new(total_supply: Balance) -> Self { + let mut balances = Mapping::default(); + let caller = Self::env().caller(); + balances.insert(&caller, &total_supply); + Self::env().emit_event(Transfer { + from: None, + to: Some(caller), + value: total_supply, + }); + Self { + total_supply, + balances, + allowances: Default::default(), + } + } + + /// Returns the total token supply. + #[ink(message)] + pub fn total_supply(&self) -> Balance { + self.total_supply + } + + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + #[ink(message)] + pub fn balance_of(&self, owner: AccountId) -> Balance { + self.balance_of_impl(&owner) + } + + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + /// + /// # Note + /// + /// Prefer to call this method over `balance_of` since this + /// works using references which are more efficient in Wasm. + #[inline] + fn balance_of_impl(&self, owner: &AccountId) -> Balance { + self.balances.get(owner).unwrap_or_default() + } + + /// Returns the amount which `spender` is still allowed to withdraw from `owner`. + /// + /// Returns `0` if no allowance has been set. + #[ink(message)] + pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance { + self.allowance_impl(&owner, &spender) + } + + /// Returns the amount which `spender` is still allowed to withdraw from `owner`. + /// + /// Returns `0` if no allowance has been set. + /// + /// # Note + /// + /// Prefer to call this method over `allowance` since this + /// works using references which are more efficient in Wasm. + #[inline] + fn allowance_impl(&self, owner: &AccountId, spender: &AccountId) -> Balance { + self.allowances.get((owner, spender)).unwrap_or_default() + } + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the caller's account balance. + #[ink(message)] + pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<()> { + let from = self.env().caller(); + self.transfer_from_to(&from, &to, value) + } + + /// Allows `spender` to withdraw from the caller's account multiple times, up to + /// the `value` amount. + /// + /// If this function is called again it overwrites the current allowance with `value`. + /// + /// An `Approval` event is emitted. + #[ink(message)] + pub fn approve(&mut self, spender: AccountId, value: Balance) -> Result<()> { + let owner = self.env().caller(); + self.allowances.insert((&owner, &spender), &value); + self.env().emit_event(Approval { + owner, + spender, + value, + }); + Ok(()) + } + + /// Transfers `value` tokens on the behalf of `from` to the account `to`. + /// + /// This can be used to allow a contract to transfer tokens on ones behalf and/or + /// to charge fees in sub-currencies, for example. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientAllowance` error if there are not enough tokens allowed + /// for the caller to withdraw from `from`. + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the account balance of `from`. + #[ink(message)] + pub fn transfer_from( + &mut self, + from: AccountId, + to: AccountId, + value: Balance, + ) -> Result<()> { + let caller = self.env().caller(); + let allowance = self.allowance_impl(&from, &caller); + if allowance < value { + return Err(Error::InsufficientAllowance) + } + self.transfer_from_to(&from, &to, value)?; + self.allowances + .insert((&from, &caller), &(allowance - value)); + Ok(()) + } + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the caller's account balance. + fn transfer_from_to( + &mut self, + from: &AccountId, + to: &AccountId, + value: Balance, + ) -> Result<()> { + let from_balance = self.balance_of_impl(from); + if from_balance < value { + return Err(Error::InsufficientBalance) + } + + self.balances.insert(from, &(from_balance - value)); + let to_balance = self.balance_of_impl(to); + self.balances.insert(to, &(to_balance + value)); + self.env().emit_event(Transfer { + from: Some(*from), + to: Some(*to), + value, + }); + Ok(()) + } + } + + #[cfg(test)] + mod tests { + use super::*; + + use ink::primitives::Clear; + + type Event = ::Type; + + fn assert_transfer_event( + event: &ink::env::test::EmittedEvent, + expected_from: Option, + expected_to: Option, + expected_value: Balance, + ) { + let decoded_event = ::decode(&mut &event.data[..]) + .expect("encountered invalid contract event data buffer"); + if let Event::Transfer(Transfer { from, to, value }) = decoded_event { + assert_eq!(from, expected_from, "encountered invalid Transfer.from"); + assert_eq!(to, expected_to, "encountered invalid Transfer.to"); + assert_eq!(value, expected_value, "encountered invalid Trasfer.value"); + } else { + panic!("encountered unexpected event kind: expected a Transfer event") + } + let expected_topics = vec![ + encoded_into_hash(&PrefixedValue { + value: b"Erc20::Transfer", + prefix: b"", + }), + encoded_into_hash(&PrefixedValue { + prefix: b"Erc20::Transfer::from", + value: &expected_from, + }), + encoded_into_hash(&PrefixedValue { + prefix: b"Erc20::Transfer::to", + value: &expected_to, + }), + encoded_into_hash(&PrefixedValue { + prefix: b"Erc20::Transfer::value", + value: &expected_value, + }), + ]; + + let topics = event.topics.clone(); + for (n, (actual_topic, expected_topic)) in + topics.iter().zip(expected_topics).enumerate() + { + let mut topic_hash = Hash::clear(); + let len = actual_topic.len(); + topic_hash.as_mut()[0..len].copy_from_slice(&actual_topic[0..len]); + + assert_eq!( + topic_hash, expected_topic, + "encountered invalid topic at {}", + n + ); + } + } + + /// The default constructor does its job. + #[ink::test] + fn new_works() { + // Constructor works. + let _erc20 = Erc20::new(100); + + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(1, emitted_events.len()); + + assert_transfer_event( + &emitted_events[0], + None, + Some(AccountId::from([0x01; 32])), + 100, + ); + } + + /// The total supply was applied. + #[ink::test] + fn total_supply_works() { + // Constructor works. + let erc20 = Erc20::new(100); + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_transfer_event( + &emitted_events[0], + None, + Some(AccountId::from([0x01; 32])), + 100, + ); + // Get the token total supply. + assert_eq!(erc20.total_supply(), 100); + } + + /// Get the actual balance of an account. + #[ink::test] + fn balance_of_works() { + // Constructor works + let erc20 = Erc20::new(100); + // Transfer event triggered during initial construction + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_transfer_event( + &emitted_events[0], + None, + Some(AccountId::from([0x01; 32])), + 100, + ); + let accounts = + ink::env::test::default_accounts::(); + // Alice owns all the tokens on contract instantiation + assert_eq!(erc20.balance_of(accounts.alice), 100); + // Bob does not owns tokens + assert_eq!(erc20.balance_of(accounts.bob), 0); + } + + #[ink::test] + fn transfer_works() { + // Constructor works. + let mut erc20 = Erc20::new(100); + // Transfer event triggered during initial construction. + let accounts = + ink::env::test::default_accounts::(); + + assert_eq!(erc20.balance_of(accounts.bob), 0); + // Alice transfers 10 tokens to Bob. + assert_eq!(erc20.transfer(accounts.bob, 10), Ok(())); + // Bob owns 10 tokens. + assert_eq!(erc20.balance_of(accounts.bob), 10); + + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(emitted_events.len(), 2); + // Check first transfer event related to ERC-20 instantiation. + assert_transfer_event( + &emitted_events[0], + None, + Some(AccountId::from([0x01; 32])), + 100, + ); + // Check the second transfer event relating to the actual trasfer. + assert_transfer_event( + &emitted_events[1], + Some(AccountId::from([0x01; 32])), + Some(AccountId::from([0x02; 32])), + 10, + ); + } + + #[ink::test] + fn invalid_transfer_should_fail() { + // Constructor works. + let mut erc20 = Erc20::new(100); + let accounts = + ink::env::test::default_accounts::(); + + assert_eq!(erc20.balance_of(accounts.bob), 0); + + // Set the contract as callee and Bob as caller. + let contract = ink::env::account_id::(); + ink::env::test::set_callee::(contract); + ink::env::test::set_caller::(accounts.bob); + + // Bob fails to transfers 10 tokens to Eve. + assert_eq!( + erc20.transfer(accounts.eve, 10), + Err(Error::InsufficientBalance) + ); + // Alice owns all the tokens. + assert_eq!(erc20.balance_of(accounts.alice), 100); + assert_eq!(erc20.balance_of(accounts.bob), 0); + assert_eq!(erc20.balance_of(accounts.eve), 0); + + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(emitted_events.len(), 1); + assert_transfer_event( + &emitted_events[0], + None, + Some(AccountId::from([0x01; 32])), + 100, + ); + } + + #[ink::test] + fn transfer_from_works() { + // Constructor works. + let mut erc20 = Erc20::new(100); + // Transfer event triggered during initial construction. + let accounts = + ink::env::test::default_accounts::(); + + // Bob fails to transfer tokens owned by Alice. + assert_eq!( + erc20.transfer_from(accounts.alice, accounts.eve, 10), + Err(Error::InsufficientAllowance) + ); + // Alice approves Bob for token transfers on her behalf. + assert_eq!(erc20.approve(accounts.bob, 10), Ok(())); + + // The approve event takes place. + assert_eq!(ink::env::test::recorded_events().count(), 2); + + // Set the contract as callee and Bob as caller. + let contract = ink::env::account_id::(); + ink::env::test::set_callee::(contract); + ink::env::test::set_caller::(accounts.bob); + + // Bob transfers tokens from Alice to Eve. + assert_eq!( + erc20.transfer_from(accounts.alice, accounts.eve, 10), + Ok(()) + ); + // Eve owns tokens. + assert_eq!(erc20.balance_of(accounts.eve), 10); + + // Check all transfer events that happened during the previous calls: + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(emitted_events.len(), 3); + assert_transfer_event( + &emitted_events[0], + None, + Some(AccountId::from([0x01; 32])), + 100, + ); + // The second event `emitted_events[1]` is an Approve event that we skip checking. + assert_transfer_event( + &emitted_events[2], + Some(AccountId::from([0x01; 32])), + Some(AccountId::from([0x05; 32])), + 10, + ); + } + + #[ink::test] + fn allowance_must_not_change_on_failed_transfer() { + let mut erc20 = Erc20::new(100); + let accounts = + ink::env::test::default_accounts::(); + + // Alice approves Bob for token transfers on her behalf. + let alice_balance = erc20.balance_of(accounts.alice); + let initial_allowance = alice_balance + 2; + assert_eq!(erc20.approve(accounts.bob, initial_allowance), Ok(())); + + // Get contract address. + let callee = ink::env::account_id::(); + ink::env::test::set_callee::(callee); + ink::env::test::set_caller::(accounts.bob); + + // Bob tries to transfer tokens from Alice to Eve. + let emitted_events_before = ink::env::test::recorded_events().count(); + assert_eq!( + erc20.transfer_from(accounts.alice, accounts.eve, alice_balance + 1), + Err(Error::InsufficientBalance) + ); + // Allowance must have stayed the same + assert_eq!( + erc20.allowance(accounts.alice, accounts.bob), + initial_allowance + ); + // No more events must have been emitted + assert_eq!( + emitted_events_before, + ink::env::test::recorded_events().count() + ) + } + + /// For calculating the event topic hash. + struct PrefixedValue<'a, 'b, T> { + pub prefix: &'a [u8], + pub value: &'b T, + } + + impl scale::Encode for PrefixedValue<'_, '_, X> + where + X: scale::Encode, + { + #[inline] + fn size_hint(&self) -> usize { + self.prefix.size_hint() + self.value.size_hint() + } + + #[inline] + fn encode_to(&self, dest: &mut T) { + self.prefix.encode_to(dest); + self.value.encode_to(dest); + } + } + + fn encoded_into_hash(entity: &T) -> Hash + where + T: scale::Encode, + { + use ink::{ + env::hash::{ + Blake2x256, + CryptoHash, + HashOutput, + }, + primitives::Clear, + }; + + let mut result = Hash::clear(); + let len_result = result.as_ref().len(); + let encoded = entity.encode(); + let len_encoded = encoded.len(); + if len_encoded <= len_result { + result.as_mut()[..len_encoded].copy_from_slice(&encoded); + return result + } + let mut hash_output = + <::Type as Default>::default(); + ::hash(&encoded, &mut hash_output); + let copy_len = core::cmp::min(hash_output.len(), len_result); + result.as_mut()[0..copy_len].copy_from_slice(&hash_output[0..copy_len]); + result + } + } +} diff --git a/ink/token_minter.ink b/ink/token_minter.ink deleted file mode 100644 index e69de29..0000000 diff --git a/python/forgery.py b/python/forgery.py index e69de29..e858683 100644 --- a/python/forgery.py +++ b/python/forgery.py @@ -0,0 +1,50 @@ +#!/usr/bin/python3 +# Author: SI + +from eth_account.account import to_standard_signature_bytes +from eth_keys import keys +from eth_utils import (big_endian_to_int, to_bytes) +from hexbytes import HexBytes +from eth_keys.backends.native.jacobian import (inv, fast_multiply, fast_add) +from eth_keys.constants import (SECPK1_G as G, SECPK1_N as N) + +def recover_public_key(message_hash, signature): + message_hash_bytes = HexBytes(message_hash) + if len(message_hash_bytes) != 32: + raise ValueError("The message hash must be exactly 32-bytes") + signature_bytes = HexBytes(signature) + signature_obj = keys.Signature(signature_bytes = to_standard_signature_bytes(signature_bytes)) + return signature_obj.recover_public_key_from_msg_hash(message_hash_bytes) + +def forge(public_key, a = 0, b = 1): + t = public_key.to_bytes() + Y = big_endian_to_int(t[:32]), big_endian_to_int(t[32:]) + + r, y = fast_add(fast_multiply(G, a), fast_multiply(Y, b)) + + s_raw = r * inv(b, N) % N + v_raw = (y % 2) ^ (0 if s_raw * 2 < N else 1) + s = s_raw if s_raw * 2 < N else N - s_raw + v = v_raw + 27 + + z = a * s_raw % N + + eth_signature_bytes = to_bytes(r).rjust(32, b'\0') + to_bytes(s).rjust(32, b'\0') + to_bytes(v) + + return '0x' + to_bytes(z).rjust(32, b'\0').hex(), '0x' + eth_signature_bytes.hex() + +hsh = '0xe50051a0af89748fe098cef3b163b6dc586a664e726791bb2a582ad364f42683' +sig = '0x2bdbc1826efc039719a28a9f4dbab9f4a2692d83de478300261a0e49019b63ee67c202ecc4ebdf82693da47824ac4fcf21f793400d85696034c4de9537c6ce491b' + +pub = recover_public_key(hsh, sig) +addr = pub.to_checksum_address() +print('recovered (checksum) address:', addr) + +a, b = 0, 1 +fhsh, fsig = forge(pub, a, b) +print('forged message hash:', fhsh) +print('forged signature:', fsig) + +fpub = recover_public_key(fhsh, fsig) +faddr = fpub.to_checksum_address() +print('recovered address check:', 'correct' if faddr == addr else 'wrong!') diff --git a/solidity/cctf9v1.sol b/solidity/cctf9v1.sol new file mode 100644 index 0000000..018dca2 --- /dev/null +++ b/solidity/cctf9v1.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// Authors: six and Silur +pragma solidity ^0.8.16; + +contract CCTF9 { + address public admin; + uint256 public volStart; + uint256 public volMaxPoints; + uint256 public powDiff; + bool public started; + + enum PlayerStatus { + Unverified, + Verified, + Banned + } + + struct Player { + PlayerStatus status; + uint256 points; + } + + modifier onlyAdmin { + require(msg.sender == admin, "Not admin"); + _; + } + + modifier onlyActive { + require(started == true, "CCTF not started."); + _; + } + + struct Flag { + address signer; + bool onlyFirstSolver; + uint256 points; + string skill_name; + } + + mapping (address => Player) public players; + mapping (uint256 => Flag) public flags; + + event CCTFStarted(uint256 timestamp); + event FlagAdded(uint256 indexed flagId, address flagSigner); + event FlagRemoved(uint256 indexed flagId); + event FlagSolved(uint256 indexed flagId, address indexed solver); + + constructor(uint256 _volMaxPoints, uint256 _powDiff) { + admin = msg.sender; + volMaxPoints = _volMaxPoints; + powDiff = _powDiff; + started = false; + } + + function setAdmin(address _admin) external onlyAdmin { + require(_admin != address(0)); + admin = _admin; + } + + function setCCTFStatus(bool _started) external onlyAdmin { + started = _started; + } + + function setFlag(uint256 _flagId, address _flagSigner, bool _onlyFirstSolver, uint256 _points, string memory _skill) external onlyAdmin{ + flags[_flagId] = Flag(_flagSigner, _onlyFirstSolver, _points, _skill); + emit FlagAdded(_flagId, _flagSigner); + } + + function setPowDiff(uint256 _powDiff) external onlyAdmin { + powDiff = _powDiff; + } + + + function register(string memory _RTFM) external { + require(players[msg.sender].status == PlayerStatus.Unverified, 'Already registered or banned'); + //uint256 pow = uint256(keccak256(abi.encodePacked("CCTF", msg.sender,"registration", nonce))); + //require(pow < powDiff, "invalid pow"); + require(keccak256(abi.encodePacked('I_read_it')) == keccak256(abi.encodePacked(_RTFM))); // PoW can be used for harder challenges, this is Entry! + players[msg.sender].status = PlayerStatus.Verified; + } + + function setPlayerStatus(address player, PlayerStatus status) external onlyAdmin { + players[player].status = status; + } + + +////////// Submit flags + mapping(bytes32 => bool) usedNs; // Against replay attack (we only check message signer) + mapping (address => mapping (uint256 => bool)) Solves; // address -> challenge ID -> solved/not + uint256 public submission_success_count = 0; // For statistics + + function SubmitFlag(bytes32 _message, bytes memory signature, uint256 _submitFor) external onlyActive { + require(players[msg.sender].status == PlayerStatus.Verified, "You are not even playing"); + require(bytes32(_message).length <= 256, "Too long message."); + require(!usedNs[_message]); + usedNs[_message] = true; + require(recoverSigner(_message, signature) == flags[_submitFor].signer, "Not signed with the correct key."); + require(Solves[msg.sender][_submitFor] == false); + + Solves[msg.sender][_submitFor] = true; + players[msg.sender].points += flags[_submitFor].points; + players[msg.sender].points = players[msg.sender].points < volMaxPoints ? players[msg.sender].points : volMaxPoints; + + if (flags[_submitFor].onlyFirstSolver) { + flags[_submitFor].points = 0; + } + + submission_success_count = submission_success_count + 1; + emit FlagSolved(_submitFor, msg.sender); + } + + function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + return ecrecover(_ethSignedMessageHash, v, r, s); + } + + function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v){ + require(sig.length == 65, "Invalid signature length"); + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + } + +////////// Check status, scores, etc + function getPlayerStatus(address _player) external view returns (PlayerStatus) { + return players[_player].status; + } + + function getPlayerPoints(address _player) external view returns (uint256) { + return players[_player].points < volMaxPoints ? players[_player].points : volMaxPoints; + } + + function getSuccessfulSubmissionCount() external view returns (uint256){ + return submission_success_count; + } +}