# DeFi
In today's tutorial, we will explore one of the original Decentralized finance (DeFi) protocols, called Uniswap v1 (opens new window). It is a protocol for an automated market maker (AMM) that automatically trades ERC20 tokens. In a traditional, centralized finance, the market maker processes an ordering book that matches buyers and sellers with their desirable prices. Uniswap attempts to automate this process with the help of the liquidity providers and arbitragers that make the price of an ERC20 token close to the market value without managing the ordering book.
# AMM
First, please take time to read the overview of the Uniswap V1 (opens new window) and its brief history (opens new window). The latest iteration of Uniswap is Uniswap V3 but, in our tutorial, we'd like to focus on the original version that demonstrates the simple idea of a constant product AMM (later).
Simply put, it determines the price (
So,
With a fee (
So,
Thanks to the fee, the
Let's see how Uniswap implements this formula in its code:
# @dev Pricing function for converting between ETH and Tokens.
# @param input_amount Amount of ETH or Tokens being sold.
# @param input_reserve Amount of ETH or Tokens (input type) in exchange reserves.
# @param output_reserve Amount of ETH or Tokens (output type) in exchange reserves.
# @return Amount of ETH or Tokens bought.
@private
@constant
def getInputPrice(input_amount: uint256, input_reserve: uint256,
output_reserve: uint256) -> uint256:
assert input_reserve > 0 and output_reserve > 0
input_amount_with_fee: uint256 = input_amount * 997
numerator: uint256 = input_amount_with_fee * output_reserve
denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee
return numerator / denominator
getInputPrice()
takes an input_amount
(i.e., selling or putting it to the liquidity pool)
and returns the amount bought (i.e., returning ether in our example).
It uses input
for selling and output
for buying in the code.
In the code, input_reserve
output_reserve
input_reserve
input_amount
) output_reserve
- return
)
It simplifies to:
return
numerator
/ denominator
where
numerator
input_amount
output_reserve
denominator
input_reserve
input_amount
# Factory
In Uniswap, anyone can create an exchange of any ERC20 token via createExchange()
.
@public
def createExchange(token: address) -> address:
assert token != ZERO_ADDRESS
assert self.exchangeTemplate != ZERO_ADDRESS
assert self.token_to_exchange[token] == ZERO_ADDRESS
exchange: address = create_with_code_of(self.exchangeTemplate)
Exchange(exchange).setup(token)
self.token_to_exchange[token] = exchange
self.exchange_to_token[exchange] = token
token_id: uint256 = self.tokenCount + 1
self.tokenCount = token_id
self.id_to_token[token_id] = token
log.NewExchange(token, exchange)
return exchange
Simply put, it creates (Exchange(exchange).setup(token)
)
a new exchange of a given ERC20 token.
Once created, any one can interact with the exchange of the ERC20 token.
However, it is created with no token or ether, as a liquidity pool,
so the original creator might want to add the proper amount of a ERC20 token
with a desirable rate of an ether
via AddLiquidity()
of Exchange
.
# @notice Deposit ETH and Tokens (self.token) at current ratio to mint UNI tokens.
# @dev min_liquidity does nothing when total UNI supply is 0.
# @param min_liquidity Minimum number of UNI sender will mint if total UNI supply is greater than 0.
# @param max_tokens Maximum number of tokens deposited. Deposits max amount if total UNI supply is 0.
# @param deadline Time after which this transaction can no longer be executed.
# @return The amount of UNI minted.
@public
@payable
def addLiquidity(min_liquidity: uint256, max_tokens: uint256,
deadline: timestamp) -> uint256:
assert deadline > block.timestamp and (max_tokens > 0 and msg.value > 0)
total_liquidity: uint256 = self.totalSupply
if total_liquidity > 0:
...
else:
assert (self.factory != ZERO_ADDRESS and self.token != ZERO_ADDRESS)
and msg.value >= 1000000000
assert self.factory.getExchange(self.token) == self
token_amount: uint256 = max_tokens
# msg.value sent to the exchange
initial_liquidity: uint256 = as_unitless_number(self.balance)
self.totalSupply = initial_liquidity
self.balances[msg.sender] = initial_liquidity
# token sent to the exchanges
assert self.token.transferFrom(msg.sender, self, token_amount)
log.AddLiquidity(msg.sender, msg.value, token_amount)
log.Transfer(ZERO_ADDRESS, msg.sender, initial_liquidity)
return initial_liquidity
addLiquidity
transfers max_tokens
(the number of tokens to put) to the exchange,
and accepts balance
(the price of the tokens) as a liquidity pool --
min_liquidity
is ignored when addLiquidity()
is invoked for the first time.
Let's try set up an Uniswap exchange (opens new window)!
$ brownie run scripts/uniswap.py -I
...
>>> factory
<Factory Contract '0xb5F338E1397D646AFFf707DD47ea94eAacdBa45D'>
# Exchange
Factory
provides a set of APIs
that create a new exchange for an ERC20 token.
You can either use the ERC20 token you created in the last tutorial
or use a generic one like below.
Note that it extends the ERC20 implementation from
OpenZeppelin (opens new window)
and allows a minting of new tokens with a non-standard mint()
interface,
but only for the owner of the contract by using the isOwner
modifier provided by Owner.sol
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC20/ERC20.sol";
import "./Owner.sol";
contract ERCToken is ERC20, Owner {
constructor(uint256 initialSupply, string memory symbol)
ERC20("General ERC20 Token", symbol) {
_mint(msg.sender, initialSupply);
}
function mint(address _receiver, uint256 amount) isOwner public {
_mint(_receiver, amount);
}
}
>>> token = ERCToken.deploy(1000, "A")
<ERCToken Contract '0xcFf3e49D8947D75Fa04115cFD3f9343c95956eb7'>
>>> factory.createExchange(token.address)
>>> factory.getExchange(token.address)
'0x0bEf07F1fF27538ee5C7D64e7582A42867069f4e'
The exchange for the token
is created with createExchange()
but it lacks an interface for its ABI.
You can populate a contract with the proper ABI with new_exchange()
provided by uniswap.py
.
>>> exchange = new_exchange(factory, token)
<Exchange Contract '0x0bEf07F1fF27538ee5C7D64e7582A42867069f4e'>
>>> exchange.tokenAddress()
'0xcFf3e49D8947D75Fa04115cFD3f9343c95956eb7'
# no liquidity pool
>>> exchange.totalSupply()
0
# no balance
>>> exchange.balance()
0
# no token
>>> token.balanceOf(exchange.address)
0
To add liquidity to the exchange,
addLiquidity()
should be invoked.
# Note.
# min_liquidity will be ignored for the very first setup
# max_tokens indicates #tokens to be put
# msg.value should be provided as part of the invocation
def addLiquidity(min_liquidity: uint256, max_tokens: uint256,
deadline: timestamp) -> uint256:
One thing you might notice is the deadline
parameter. If the transaction's
block.timestamp
is not within the provided deadline, uniswap will revert it.
This can help prevent funds from beiing locked away and traded at an unfavorable
price in case your transaction takes a long time to confirm.
It might also be there to mitigate a front running (or a sandwich attack)
that manipulates the price of the token in the liquidity pool
before the victim's transaction executes.
A tight deadline
can render such attack invalid as
the attacker attempts to delay the commit of the transaction,
but I doubt it is really effective in practice --
modern bots can quickly examine the transactions
and insert their own on the fly.
# allow the exchange to spend 100 tokens
>>> token.approve(exchange.address, 100)
# add liquidity of (100 tokens, 1000000000 wei)
>>> tx = exchange.addLiquidity(0, 100, int(time.time()) + 1000, {"value": 1000000000})
>>> tx.info()
Transaction was Mined
---------------------
Tx Hash: 0x799b9ad7e2d4f00b2b386c97e1b244688e038d209af0e9f9854c8794ab655528
From: 0x66aB6D9362d4F35596279692F0251Db635165871
To: 0x0bEf07F1fF27538ee5C7D64e7582A42867069f4e
Value: 1000000000
Function: Transaction
Block: 2183
Gas Used: 102307 / 12000000 (0.9%)
Events In This Transaction
--------------------------
├── General ERC20 Token (0xcFf3e49D8947D75Fa04115cFD3f9343c95956eb7)
│ ├── Approval
│ │ ├── owner: 0x66aB6D9362d4F35596279692F0251Db635165871
│ │ ├── spender: 0x0bEf07F1fF27538ee5C7D64e7582A42867069f4e
│ │ └── value: 0
│ └── Transfer
│ ├── from: 0x66aB6D9362d4F35596279692F0251Db635165871
│ ├── to: 0x0bEf07F1fF27538ee5C7D64e7582A42867069f4e
│ └── value: 100
│
└── 0x0bEf07F1fF27538ee5C7D64e7582A42867069f4e
├── AddLiquidity
│ ├── provider: 0x66aB6D9362d4F35596279692F0251Db635165871
│ ├── eth_amount: 1000000000
│ └── token_amount: 100
└── Transfer
├── _from: 0x0000000000000000000000000000000000000000
├── _to: 0x66aB6D9362d4F35596279692F0251Db635165871
└── _value: 1000000000
# we can also examine their balance and #tokens
>>> exchange.balance()
1000000000
>>> exchange.balanceOf(a[0].address)
1000000000
>>> exchange.totalSupply()
1000000000
>>> token.balanceOf(exchange.address)
100
# ERC20 ↔ ETH
Let's interact with the exchange! So far, we added (100 Tokens, 1000000000 Wei) to the liquidity pool
and so the price of a token would be 1000000000 wei / 100 token = 10000000 wei/token.
To check the price of the token when selling (i.e., inputting to the exchange),
getTokenToEthInputPrice()
can be used:
# @notice Public price function for Token to ETH trades with an exact input.
# @param tokens_sold Amount of Tokens sold.
# @return Amount of ETH that can be bought with input Tokens.
@public
@constant
def getTokenToEthInputPrice(tokens_sold: uint256) -> uint256(wei):
assert tokens_sold > 0
token_reserve: uint256 = self.token.balanceOf(self)
eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance))
return as_wei_value(eth_bought, 'wei')
In fact, it's a simple wrapper of getInputPrice()
we studied and, given a token to be sold,
it returns the amount of ETH can be bought (i.e., outputting ether).
>>> exchange.getTokenToEthInputPrice(1)
9871580
>>> 10000000 - 9871580
128420
With the fee, it returns slightly lesser amount than the ideal, 10000000 wei/token.
Let's see how the token price changes if we exchanges a token with ETH
by using tokenToEthSwapInput()
:
# @notice Convert Tokens to ETH.
# @dev User specifies exact input and minimum output.
# @param tokens_sold Amount of Tokens sold.
# @param min_eth Minimum ETH purchased.
# @param deadline Time after which this transaction can no longer be executed.
# @return Amount of ETH bought.
@public
def tokenToEthSwapInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp) -> uint256(wei):
return self.tokenToEthInput(tokens_sold, min_eth, deadline, msg.sender, msg.sender)
It accepts the number of the token to be sold (i.e., inputting to the exchange)
and returns the ether back to the msg.sender
.
# putting a token to the exchange and gets the ether back
>>> ethPerToken = exchange.getTokenToEthInputPrice(1)
>>> token.approve(exchange.address, 1)
>>> tx = exchange.tokenToEthSwapInput(1, ethPerToken, int(time.time())+1000)
Transaction sent: 0x65880fbeb446f9edab8d26062c767541e67516ceea65310e4a3f82e1d18be683
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 9
Transaction confirmed Block: 10 Gas used: 51723 (0.43%)
<Transaction '0x65880fbeb446f9edab8d26062c767541e67516ceea65310e4a3f82e1d18be683'>
>>> tx.info()
Transaction was Mined
---------------------
Tx Hash: 0x65880fbeb446f9edab8d26062c767541e67516ceea65310e4a3f82e1d18be683
From: 0x66aB6D9362d4F35596279692F0251Db635165871
To: 0x87e23022F31814f835C052A9e280608bdE7aE3f9
Value: 0
Function: Transaction
Block: 10
Gas Used: 51723 / 12000000 (0.4%)
Events In This Transaction
--------------------------
├── General ERC20 Token (0xe0aA552A10d7EC8760Fc6c246D391E698a82dDf9)
│ ├── Approval
│ │ ├── owner: 0x66aB6D9362d4F35596279692F0251Db635165871
│ │ ├── spender: 0x87e23022F31814f835C052A9e280608bdE7aE3f9
│ │ └── value: 0
│ └── Transfer
│ ├── from: 0x66aB6D9362d4F35596279692F0251Db635165871
│ ├── to: 0x87e23022F31814f835C052A9e280608bdE7aE3f9
│ └── value: 1
│
└── 0x87e23022F31814f835C052A9e280608bdE7aE3f9
└── EthPurchase
├── buyer: 0x66aB6D9362d4F35596279692F0251Db635165871
├── tokens_sold: 1
└── eth_bought: 9871580
# new balance
>>> exchange.balance()
990128420
# one more token!
>>> token.balanceOf(exchange.address)
101
# new price!
>>> exchange.getTokenToEthInputPrice(1)
9678304
The exchange now has one more tokens so its price goes down (i.e., attracting arbitragers to buy the token) a bit.
from scripts.pwn import *
from scripts.uniswap import *
import time
def new_exchange_instance(factory):
# 1000 initial tokens
token = ERCToken.deploy(1000, "A")
exchange = new_exchange(factory, token)
# 100 to 1000000000 (wei) rate
token.approve(exchange.address, 100)
exchange.addLiquidity(0, 100, int(time.time()) + 1000, {"value": 1000000000})
return (token, exchange)
def main():
accounts.default = a[0]
factory = init_uniswap()
prices = []
# collecting prices when increasing #token
(token, exchange) = new_exchange_instance(factory)
for i in range(50):
ethPerToken = exchange.getTokenToEthInputPrice(1)
num_token = token.balanceOf(exchange.address)
eth_price = exchange.balance()
prices.append((num_token, eth_price, num_token * eth_price))
token.approve(exchange.address, 1)
exchange.tokenToEthSwapInput(1, ethPerToken, int(time.time())+1000)
# collecting prices when decreasing #token
(token, exchange) = new_exchange_instance(factory)
for i in range(50):
ethPerToken = exchange.getEthToTokenOutputPrice(1)
num_token = token.balanceOf(exchange.address)
eth_price = exchange.balance()
prices.append((num_token, eth_price, num_token * eth_price))
exchange.ethToTokenSwapOutput(1, int(time.time())+1000, {"value": ethPerToken})
# draw figures
prices = sorted(prices)
import matplotlib.pyplot as plt
import numpy as np
orig_k = 100 * 1000000000.0
tokens = [p[0] for p in prices]
eths = [p[1] for p in prices]
ks = [p[2] for p in prices]
fig, (ax0, ax1) = plt.subplots(nrows=2, ncols=1, sharex=True)
ax0.set_title("(token, eth)")
ax0.set_xlim((50, 150))
ax0.plot(tokens, eths)
ax1.set_title("k=token * eth (drift)")
ax1.set_ylim((0.99*orig_k, 1.01*orig_k))
ax0.set_xlim((50, 150))
ax1.plot(tokens, ks)
plt.show()
# ERC20 ↔ ERC20
Uniswap provides a means to convert ERC20 ↔ ETH,
and by using ETH as a common basis,
it can also convert ERC20 ↔ ERC20.
If you'd like to explore, please check the contracts/uniswap-v1/uniswap_exchange.vy
!
# @notice Convert Tokens (self.token) to Tokens (token_addr).
# @dev User specifies exact input and minimum output.
# @param tokens_sold Amount of Tokens sold.
# @param min_tokens_bought Minimum Tokens (token_addr) purchased.
# @param min_eth_bought Minimum ETH purchased as intermediary.
# @param deadline Time after which this transaction can no longer be executed.
# @param token_addr The address of the token being purchased.
# @return Amount of Tokens (token_addr) bought.
@public
def tokenToTokenSwapInput(tokens_sold: uint256, min_tokens_bought: uint256, min_eth_bought: uint256(wei),
deadline: timestamp, token_addr: address) -> uint256:
exchange_addr: address = self.factory.getExchange(token_addr)
return self.tokenToTokenInput(tokens_sold, min_tokens_bought, min_eth_bought,
deadline, msg.sender, msg.sender, exchange_addr)
# New Upgrade: ERC777 (opens new window)
ERC777 (opens new window) is an upgraded version of the ERC20 standard that maintains compatibility to ERC20. Take time to explore its EIP (opens new window) and an implementation in a popular library (opens new window).
The crux of ERC777 is to provide
❶ advanced features like hooks via ERC1820 (opens new window)
and ❷ maintains the compatibility layer to ERC20 (opens new window).
Simply put,
it calls back to the preregistered contract
whenever a token sends out (ERC777TokensSender
)
or whenever it receives a token (ERC777TokensRecipient
).
The registration of these hooks are done by a single contract, called ERC1820 (opens new window) ---
its implementation and the idea of maintaining
a common, static address across different chains are super interesting.
If you are interested in, please check the suggested
deployment method (opens new window) of ERC1820.
One can imagine an advanced feature
like performing an programmable access control of the ERC20
on top of the hooks, for example.
contract ERC777 is Context, IERC777, IERC20 {
...
function transferFrom(address holder, address recipient, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(holder, spender, amount);
* _send(holder, recipient, amount, "", "", false);
return true;
}
function _send(
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData,
bool requireReceptionAck
) internal virtual {
require(from != address(0), "ERC777: transfer from the zero address");
require(to != address(0), "ERC777: transfer to the zero address");
address operator = _msgSender();
* _callTokensToSend(operator, from, to, amount, userData, operatorData);
_move(operator, from, to, amount, userData, operatorData);
_callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);
}
function _callTokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData
) private {
* address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(from, _TOKENS_SENDER_INTERFACE_HASH);
if (implementer != address(0)) {
* IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
}
}
}
Let's follow the code snippets marked *
.
When transferFrom()
is invoked,
_send()_
and _callTokensToSend()
are invoked inside.
And, in _callTokensToSend()
,
it looks up the registered call
and invoked tokensToSend()
of the recipient.
Why suddenly are talking about ERC777
in Uniswap?
Remember, it's compatible to ERC20!
It means a ERC777 token can be used as a regular ERC20 token in Uniswap
and any one can create an exchange for the token! Nice.
Sadly, Uniswap V1 is not built to handle an advanced feature
in a secure manner.
More specifically,
an attacker interpose the execution of tokenToEthInput()
right before the token is transferred to the exchange:
@private
def tokenToEthInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp, buyer: address, recipient: address) -> uint256(wei):
assert deadline >= block.timestamp and (tokens_sold > 0 and min_eth > 0)
* token_reserve: uint256 = self.token.balanceOf(self)
eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance))
wei_bought: uint256(wei) = as_wei_value(eth_bought, 'wei')
assert wei_bought >= min_eth
send(recipient, wei_bought)
* assert self.token.transferFrom(buyer, self, tokens_sold)
log.EthPurchase(buyer, tokens_sold, wei_bought)
return wei_bought
It means, while transferFrom()
is executing
(i.e., a token is not yet transferred),
one can invoke another transactions,
whose balanceOf()
is not yet reflecting the past transaction.
Task
By selling 50 tokens, how much can you receive from the exchange? If you start selling 1 token at a time, its token price decreases quickly. You only can get 332525206 wei if you sell one token at a time.
# eth per token
>>> exchange.getTokenToEthInputPrice(1)
9871580
>>> token.approve(exchange.address, 50)
>>> for i in range(50): exchange.tokenToEthSwapInput(1, 1, time.time()+1000)
# how much did we get?
>>> 1000000000 - exchange.balance()
332525206
# eth per token after selling 50
>>> exchange.getTokenToEthInputPrice(1)
4407189
What about selling 50 together? We can get a bit more -- plus, avoiding some gas fee.
>>> exchange.getTokenToEthInputPrice(50)
332665999
>>> token.approve(exchange.address, 50)
>>> exchange.tokenToEthSwapInput(50, 1, time.time()+1000)
# how much did we get?
>>> 1000000000 - exchange.balance()
332665999
Can you sell 50 tokens pricier than this? Say 350000000?
contract Lab06 {
ERC777Token public token;
UniswapExchange public exchange;
UniswapFactory public factory;
constructor(address _player) payable {
require(msg.value == 1000000000);
address[] memory defaultOperators = new address[](1);
defaultOperators[0] = address(this);
token = new ERC777Token(1000, defaultOperators);
factory = UniswapFactory(0x6B6aB591396f868bda7c42899Fc58A9E4F4AC3B8);
exchange = UniswapExchange(factory.createExchange(address(token)));
// give you 50 tokens to play with!
token.transfer(_player, 50);
// put 100 tokens to the liquidity pool
token.approve(address(exchange), 100);
exchange.addLiquidity{value: 1000000000}(0, 100, block.timestamp+1000);
}
function completed() public view returns (bool) {
return token.balanceOf(address(exchange)) == 150
&& address(exchange).balance < (1000000000 - 350000000);
}
}
ERC777 Token
You can find ERC777 token and related code in the repository!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC777/ERC777.sol";
import "./Owner.sol";
contract AdvERCToken is ERC777, Owner {
constructor(uint256 initialSupply,
string memory symbol,
address[] memory defaultOperators)
ERC777("General ERC777 Token", symbol, defaultOperators) {
_mint(msg.sender, initialSupply, "", "");
}
function mint(address _receiver, uint256 amount) isOwner public {
_mint(_receiver, amount, "", "");
}
}
>>> token = AdvERCToken.deploy(1000, "B", [a[0].address])
# it works like ERC20
>>> token.balanceOf(a[0].address)
1000
ERC1820
In Lab06Exploit.sol
, you can find a template of ERC777
that uses ERC1820 interfaces. Please digest the template code
and launches your exploit!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
import "./ERC777/IERC777Sender.sol";
import "./ERC777/IERC1820Registry.sol";
import "./ERC20/IERC20.sol";
interface UniswapExchange {
function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256);
function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256);
}
contract Lab06Exploit is IERC777Sender {
UniswapExchange private exchange;
IERC20 private token;
IERC1820Registry private ERC1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
address owner;
constructor(address _exchangeAddress, address _tokenAddress) public {
exchange = UniswapExchange(_exchangeAddress);
token = IERC20(_tokenAddress);
owner = msg.sender;
// register a hook
ERC1820.setInterfaceImplementer(address(this), keccak256("ERC777TokensSender"), address(this));
}
// ERC777 hook
event Log(string log);
function tokensToSend(address, address, address, uint256, bytes calldata, bytes calldata) external {
emit Log("triggered?");
}
function testMe() external {
token.approve(address(exchange), 1);
exchange.tokenToEthSwapInput(1, 1, block.timestamp * 2);
}
receive() external payable {}
}
If testMe()
is invoked,
you can find "triggered?" in the log,
which means the tokensToSend()
call back is invoked
in the middle of Uniswap's transaction.
>>> x = Lab06Exploit.deploy(exchange.address, token.address)
>>> token.transfer(x.address, 1)
>>> tx = x.testMe()
>>> tx.info()
Transaction was Mined
---------------------
Tx Hash: 0x3aad72dbb1bd23f4c859cd11cabec36504c5b89f75b0b9b255fdf5f99848ada4
From: 0xfed61D4a212A14143DC51f21d8D6617072e7a621
To: 0x16a318b60Ca20Ab4d538cf9f96C8f7817F9e4175
Value: 0
Function: Lab06Exploit.testMe
Block: 2197
Gas Used: 84067 / 12000000 (0.7%)
Events In This Transaction
--------------------------
├── General ERC777 Token (0x03f3621F38E44E099f704D08f3132A46d4329946)
│ ├── Approval
│ │ ├── owner: 0x16a318b60Ca20Ab4d538cf9f96C8f7817F9e4175
│ │ ├── spender: 0x9055210007e68928C8Db8A981153Da34633ceba8
│ │ └── value: 1
│ └── Approval
│ ├── owner: 0x16a318b60Ca20Ab4d538cf9f96C8f7817F9e4175
│ ├── spender: 0x9055210007e68928C8Db8A981153Da34633ceba8
│ └── value: 0
│
├── Lab06Exploit (0x16a318b60Ca20Ab4d538cf9f96C8f7817F9e4175)
│ └── Log
│ └── log: triggered?
│
├── General ERC777 Token (0x03f3621F38E44E099f704D08f3132A46d4329946)
│ ├── Sent
│ │ ├── operator: 0x9055210007e68928C8Db8A981153Da34633ceba8
│ │ ├── from: 0x16a318b60Ca20Ab4d538cf9f96C8f7817F9e4175
│ │ ├── to: 0x9055210007e68928C8Db8A981153Da34633ceba8
│ │ ├── amount: 1
│ │ ├── data: 0x00
│ │ └── operatorData: 0x00
│ └── Transfer
│ ├── from: 0x16a318b60Ca20Ab4d538cf9f96C8f7817F9e4175
│ ├── to: 0x9055210007e68928C8Db8A981153Da34633ceba8
│ └── value: 1
│
└── 0x9055210007e68928C8Db8A981153Da34633ceba8
└── EthPurchase
├── buyer: 0x16a318b60Ca20Ab4d538cf9f96C8f7817F9e4175
├── tokens_sold: 1
└── eth_bought: 9871580
← Token Bank Donation →