Receipt reuse and forgery tools

main
six 2022-10-06 17:17:05 +02:00
parent f72bbff99d
commit 596e8dec08
6 changed files with 771 additions and 25 deletions

View File

@ -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/

26
ink/Cargo.toml 100644
View File

@ -0,0 +1,26 @@
[package]
name = "erc20"
version = "4.0.0-alpha.3"
authors = ["Parity Technologies <admin@parity.io>"]
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 = []

532
ink/lib.rs 100644
View File

@ -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<AccountId, Balance>,
/// 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<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
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<T> = core::result::Result<T, Error>;
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 = <Erc20 as ::ink::reflect::ContractEventBase>::Type;
fn assert_transfer_event(
event: &ink::env::test::EmittedEvent,
expected_from: Option<AccountId>,
expected_to: Option<AccountId>,
expected_value: Balance,
) {
let decoded_event = <Event as scale::Decode>::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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
assert_transfer_event(
&emitted_events[0],
None,
Some(AccountId::from([0x01; 32])),
100,
);
let accounts =
ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
// 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::<ink::env::DefaultEnvironment>();
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::<Vec<_>>();
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::<ink::env::DefaultEnvironment>();
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::DefaultEnvironment>();
ink::env::test::set_callee::<ink::env::DefaultEnvironment>(contract);
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(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::<Vec<_>>();
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::<ink::env::DefaultEnvironment>();
// 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::DefaultEnvironment>();
ink::env::test::set_callee::<ink::env::DefaultEnvironment>(contract);
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(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::<Vec<_>>();
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::<ink::env::DefaultEnvironment>();
// 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::DefaultEnvironment>();
ink::env::test::set_callee::<ink::env::DefaultEnvironment>(callee);
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(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<X> 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<T: scale::Output + ?Sized>(&self, dest: &mut T) {
self.prefix.encode_to(dest);
self.value.encode_to(dest);
}
}
fn encoded_into_hash<T>(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 =
<<Blake2x256 as HashOutput>::Type as Default>::default();
<Blake2x256 as CryptoHash>::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
}
}
}

View File

View File

@ -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!')

View File

@ -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;
}
}