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)
|
#### Scenario 2: Signature forgery (any chain)
|
||||||
- Bridge providing receipts || Checks bridge balance on Substrate node
|
- Deploy SC on Ethereum chain
|
||||||
- Ethereum node || Token minter smart contract (target for mint)
|
- Compile Substrate with EVM
|
||||||
|
- Deploy SC
|
||||||
|
- Test ECDSA signature forgery exploit from one to other
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
#### Scenario 1
|
||||||
https://remix.ethereum.org/
|
https://remix.ethereum.org/
|
||||||
Faucet?
|
https://www.tutorialspoint.com/solidity/solidity_operators.htm
|
||||||
https://polkadot.js.org/apps/
|
https://polkadot.js.org/apps/
|
||||||
https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
|
https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
|
||||||
https://git.hsbp.org/six/eth_keygen
|
https://git.hsbp.org/six/eth_keygen
|
||||||
|
|
||||||
|
#### Scenario 2
|
||||||
https://github.com/paritytech/substrate-contracts-node
|
https://github.com/paritytech/substrate-contracts-node
|
||||||
https://docs.substrate.io/quick-start/
|
https://docs.substrate.io/quick-start/
|
||||||
https://substrate.io/developers/playground/ | alternative
|
|
||||||
https://github.com/substrate-developer-hub/substrate-front-end-template
|
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/ink
|
||||||
|
https://github.com/paritytech/substrate/blob/master/primitives/core/src/ecdsa.rs
|
||||||
https://use.ink/getting-started/setup
|
https://use.ink/getting-started/setup
|
||||||
https://medium.com/block-journal/introducing-substrate-smart-contracts-with-ink-d486289e2b59
|
https://medium.com/block-journal/introducing-substrate-smart-contracts-with-ink-d486289e2b59
|
||||||
|
https://substrate.io/developers/playground/
|
||||||
## Commands
|
|
||||||
$
|
|
||||||
|
|
|
@ -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