Receipt reuse and forgery tools
parent
f72bbff99d
commit
596e8dec08
50
README.md
50
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/
|
||||
|
|
|
@ -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 = []
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!')
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue