链上信息、资产标准与协议拓展

Hacker Dōjo Workshop:
研究种类:课题研究
资助金额:120 USDT
分享者:十四君 Web2 五年网络安全与黑灰产业研究,Github 7K Star
现 Web3 专注研究Market、DeFi、Geth ,技术向周更博主,公众号《十四君》
本项目由Hacker Dōjo资助,文章转载请联系
Telegram: @DoraDojo0
WeChat: @HackerDojo0

从公开私钥的陷阱说起

https://etherscan.io/address/0xb89d8a7c56241b550A6f8a0938BBBB2E2fe3166F

问答

  • 获得私钥真能转走这30W刀吗?

    不能,有USDT 黑名单

  • 转走与否和我手速快慢有关系吗?

    无关,以太坊出块15s,内存池是公开的

    • 传统抢购,先到先得,演出票等要么是内鬼,要么是近距离服务器秒杀。
    • 链上抢购,价高者得,内存池内是基本公开,矿工可以排序交易。

  • 谁是最终受益者,他赚了多少?

  • 拓展问题

    后来资金确实被转走了,如何做到?

区块链的信息

问题:他为什么能画出来这样的动画图?

1. 如何得知交易与左侧应用的关系
2. 交易签名是没有时间的,那如何统计出下方长期panding的交易?
3. 每个车厢为什么会有出来的交易?
  • 区块信息详解

问题:

1. 该笔交易失败,该笔交易涉及的数据就会全部回滚吗?
2. MethodId 是如何算出来?

标准资产协议

  • ERC20标准是什么?

    ERC-20: Token Standard

  • 接口概览

    // SPDX-License-Identifier: UNLICENSED
    pragma solidity >=0.5.0;
    
    interface IERC20 {
        event Approval(address indexed owner, address indexed spender, uint value);
        event Transfer(address indexed from, address indexed to, uint value);
    
        function name() external view returns (string memory);
        function symbol() external view returns (string memory);
        function decimals() external view returns (uint8);
        function totalSupply() external view returns (uint);
        function balanceOf(address owner) external view returns (uint);
        function allowance(address owner, address spender) external view returns (uint);
    
        function approve(address spender, uint value) external returns (bool);
        function transfer(address to, uint value) external returns (bool);
        function transferFrom(address from, address to, uint value) external returns (bool);
    
    }
    
  • 完整实现-OpenZeppelin

    /**
     *Submitted for verification at testnet.bscscan.com on 2023-08-13
    */
    
    // File: @openzeppelin/contracts/utils/Context.sol
    
    // OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
    
    pragma solidity ^0.8.0;
    
    /**
     * @dev Provides information about the current execution context, including the
     * sender of the transaction and its data. While these are generally available
     * via msg.sender and msg.data, they should not be accessed in such a direct
     * manner, since when dealing with meta-transactions the account sending and
     * paying for execution may not be the actual sender (as far as an application
     * is concerned).
     *
     * This contract is only required for intermediate, library-like contracts.
     */
    abstract contract Context {
        function _msgSender() internal view virtual returns (address) {
            return msg.sender;
        }
    
        function _msgData() internal view virtual returns (bytes calldata) {
            return msg.data;
        }
    }
    
    // File: @openzeppelin/contracts/token/ERC20/IERC20.sol
    
    // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)
    
    pragma solidity ^0.8.0;
    
    /**
     * @dev Interface of the ERC20 standard as defined in the EIP.
     */
    interface IERC20 {
        /**
         * @dev Emitted when `value` tokens are moved from one account (`from`) to
         * another (`to`).
         *
         * Note that `value` may be zero.
         */
        event Transfer(address indexed from, address indexed to, uint256 value);
    
        /**
         * @dev Emitted when the allowance of a `spender` for an `owner` is set by
         * a call to {approve}. `value` is the new allowance.
         */
        event Approval(address indexed owner, address indexed spender, uint256 value);
    
        /**
         * @dev Returns the amount of tokens in existence.
         */
        function totalSupply() external view returns (uint256);
    
        /**
         * @dev Returns the amount of tokens owned by `account`.
         */
        function balanceOf(address account) external view returns (uint256);
    
        /**
         * @dev Moves `amount` tokens from the caller's account to `to`.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * Emits a {Transfer} event.
         */
        function transfer(address to, uint256 amount) external returns (bool);
    
        /**
         * @dev Returns the remaining number of tokens that `spender` will be
         * allowed to spend on behalf of `owner` through {transferFrom}. This is
         * zero by default.
         *
         * This value changes when {approve} or {transferFrom} are called.
         */
        function allowance(address owner, address spender) external view returns (uint256);
    
        /**
         * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * IMPORTANT: Beware that changing an allowance with this method brings the risk
         * that someone may use both the old and the new allowance by unfortunate
         * transaction ordering. One possible solution to mitigate this race
         * condition is to first reduce the spender's allowance to 0 and set the
         * desired value afterwards:
         * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
         *
         * Emits an {Approval} event.
         */
        function approve(address spender, uint256 amount) external returns (bool);
    
        /**
         * @dev Moves `amount` tokens from `from` to `to` using the
         * allowance mechanism. `amount` is then deducted from the caller's
         * allowance.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * Emits a {Transfer} event.
         */
        function transferFrom(address from, address to, uint256 amount) external returns (bool);
    }
    
    // File: @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol
    
    // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)
    
    pragma solidity ^0.8.0;
    
    /**
     * @dev Interface for the optional metadata functions from the ERC20 standard.
     *
     * _Available since v4.1._
     */
    interface IERC20Metadata is IERC20 {
        /**
         * @dev Returns the name of the token.
         */
        function name() external view returns (string memory);
    
        /**
         * @dev Returns the symbol of the token.
         */
        function symbol() external view returns (string memory);
    
        /**
         * @dev Returns the decimals places of the token.
         */
        function decimals() external view returns (uint8);
    }
    
    // File: @openzeppelin/contracts/token/ERC20/ERC20.sol
    
    // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol)
    
    pragma solidity ^0.8.0;
    
    /**
     * @dev Implementation of the {IERC20} interface.
     *
     * This implementation is agnostic to the way tokens are created. This means
     * that a supply mechanism has to be added in a derived contract using {_mint}.
     * For a generic mechanism see {ERC20PresetMinterPauser}.
     *
     * TIP: For a detailed writeup see our guide
     * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
     * to implement supply mechanisms].
     *
     * The default value of {decimals} is 18. To change this, you should override
     * this function so it returns a different value.
     *
     * We have followed general OpenZeppelin Contracts guidelines: functions revert
     * instead returning `false` on failure. This behavior is nonetheless
     * conventional and does not conflict with the expectations of ERC20
     * applications.
     *
     * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
     * This allows applications to reconstruct the allowance for all accounts just
     * by listening to said events. Other implementations of the EIP may not emit
     * these events, as it isn't required by the specification.
     *
     * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
     * functions have been added to mitigate the well-known issues around setting
     * allowances. See {IERC20-approve}.
     */
    contract ERC20 is Context, IERC20, IERC20Metadata {
        mapping(address => uint256) private _balances;
    
        mapping(address => mapping(address => uint256)) private _allowances;
    
        uint256 private _totalSupply;
    
        string private _name;
        string private _symbol;
    
        /**
         * @dev Sets the values for {name} and {symbol}.
         *
         * All two of these values are immutable: they can only be set once during
         * construction.
         */
        constructor(string memory name_, string memory symbol_) {
            _name = name_;
            _symbol = symbol_;
        }
    
        /**
         * @dev Returns the name of the token.
         */
        function name() public view virtual override returns (string memory) {
            return _name;
        }
    
        /**
         * @dev Returns the symbol of the token, usually a shorter version of the
         * name.
         */
        function symbol() public view virtual override returns (string memory) {
            return _symbol;
        }
    
        /**
         * @dev Returns the number of decimals used to get its user representation.
         * For example, if `decimals` equals `2`, a balance of `505` tokens should
         * be displayed to a user as `5.05` (`505 / 10 ** 2`).
         *
         * Tokens usually opt for a value of 18, imitating the relationship between
         * Ether and Wei. This is the default value returned by this function, unless
         * it's overridden.
         *
         * NOTE: This information is only used for _display_ purposes: it in
         * no way affects any of the arithmetic of the contract, including
         * {IERC20-balanceOf} and {IERC20-transfer}.
         */
        function decimals() public view virtual override returns (uint8) {
            return 18;
        }
    
        /**
         * @dev See {IERC20-totalSupply}.
         */
        function totalSupply() public view virtual override returns (uint256) {
            return _totalSupply;
        }
    
        /**
         * @dev See {IERC20-balanceOf}.
         */
        function balanceOf(address account) public view virtual override returns (uint256) {
            return _balances[account];
        }
    
        /**
         * @dev See {IERC20-transfer}.
         *
         * Requirements:
         *
         * - `to` cannot be the zero address.
         * - the caller must have a balance of at least `amount`.
         */
        function transfer(address to, uint256 amount) public virtual override returns (bool) {
            address owner = _msgSender();
            _transfer(owner, to, amount);
            return true;
        }
    
        /**
         * @dev See {IERC20-allowance}.
         */
        function allowance(address owner, address spender) public view virtual override returns (uint256) {
            return _allowances[owner][spender];
        }
    
        /**
         * @dev See {IERC20-approve}.
         *
         * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
         * `transferFrom`. This is semantically equivalent to an infinite approval.
         *
         * Requirements:
         *
         * - `spender` cannot be the zero address.
         */
        function approve(address spender, uint256 amount) public virtual override returns (bool) {
            address owner = _msgSender();
            _approve(owner, spender, amount);
            return true;
        }
    
        /**
         * @dev See {IERC20-transferFrom}.
         *
         * Emits an {Approval} event indicating the updated allowance. This is not
         * required by the EIP. See the note at the beginning of {ERC20}.
         *
         * NOTE: Does not update the allowance if the current allowance
         * is the maximum `uint256`.
         *
         * Requirements:
         *
         * - `from` and `to` cannot be the zero address.
         * - `from` must have a balance of at least `amount`.
         * - the caller must have allowance for ``from``'s tokens of at least
         * `amount`.
         */
        function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
            address spender = _msgSender();
            _spendAllowance(from, spender, amount);
            _transfer(from, to, amount);
            return true;
        }
    
        /**
         * @dev Atomically increases the allowance granted to `spender` by the caller.
         *
         * This is an alternative to {approve} that can be used as a mitigation for
         * problems described in {IERC20-approve}.
         *
         * Emits an {Approval} event indicating the updated allowance.
         *
         * Requirements:
         *
         * - `spender` cannot be the zero address.
         */
        function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
            address owner = _msgSender();
            _approve(owner, spender, allowance(owner, spender) + addedValue);
            return true;
        }
    
        /**
         * @dev Atomically decreases the allowance granted to `spender` by the caller.
         *
         * This is an alternative to {approve} that can be used as a mitigation for
         * problems described in {IERC20-approve}.
         *
         * Emits an {Approval} event indicating the updated allowance.
         *
         * Requirements:
         *
         * - `spender` cannot be the zero address.
         * - `spender` must have allowance for the caller of at least
         * `subtractedValue`.
         */
        function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
            address owner = _msgSender();
            uint256 currentAllowance = allowance(owner, spender);
            require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
            unchecked {
                _approve(owner, spender, currentAllowance - subtractedValue);
            }
    
            return true;
        }
    
        /**
         * @dev Moves `amount` of tokens from `from` to `to`.
         *
         * This internal function is equivalent to {transfer}, and can be used to
         * e.g. implement automatic token fees, slashing mechanisms, etc.
         *
         * Emits a {Transfer} event.
         *
         * Requirements:
         *
         * - `from` cannot be the zero address.
         * - `to` cannot be the zero address.
         * - `from` must have a balance of at least `amount`.
         */
        function _transfer(address from, address to, uint256 amount) internal virtual {
            require(from != address(0), "ERC20: transfer from the zero address");
            require(to != address(0), "ERC20: transfer to the zero address");
    
            _beforeTokenTransfer(from, to, amount);
    
            uint256 fromBalance = _balances[from];
            require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
            unchecked {
                _balances[from] = fromBalance - amount;
                // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
                // decrementing then incrementing.
                _balances[to] += amount;
            }
    
            emit Transfer(from, to, amount);
    
            _afterTokenTransfer(from, to, amount);
        }
    
        /** @dev Creates `amount` tokens and assigns them to `account`, increasing
         * the total supply.
         *
         * Emits a {Transfer} event with `from` set to the zero address.
         *
         * Requirements:
         *
         * - `account` cannot be the zero address.
         */
        function _mint(address account, uint256 amount) internal virtual {
            require(account != address(0), "ERC20: mint to the zero address");
    
            _beforeTokenTransfer(address(0), account, amount);
    
            _totalSupply += amount;
            unchecked {
                // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
                _balances[account] += amount;
            }
            emit Transfer(address(0), account, amount);
    
            _afterTokenTransfer(address(0), account, amount);
        }
    
        /**
         * @dev Destroys `amount` tokens from `account`, reducing the
         * total supply.
         *
         * Emits a {Transfer} event with `to` set to the zero address.
         *
         * Requirements:
         *
         * - `account` cannot be the zero address.
         * - `account` must have at least `amount` tokens.
         */
        function _burn(address account, uint256 amount) internal virtual {
            require(account != address(0), "ERC20: burn from the zero address");
    
            _beforeTokenTransfer(account, address(0), amount);
    
            uint256 accountBalance = _balances[account];
            require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
            unchecked {
                _balances[account] = accountBalance - amount;
                // Overflow not possible: amount <= accountBalance <= totalSupply.
                _totalSupply -= amount;
            }
    
            emit Transfer(account, address(0), amount);
    
            _afterTokenTransfer(account, address(0), amount);
        }
    
        /**
         * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
         *
         * This internal function is equivalent to `approve`, and can be used to
         * e.g. set automatic allowances for certain subsystems, etc.
         *
         * Emits an {Approval} event.
         *
         * Requirements:
         *
         * - `owner` cannot be the zero address.
         * - `spender` cannot be the zero address.
         */
        function _approve(address owner, address spender, uint256 amount) internal virtual {
            require(owner != address(0), "ERC20: approve from the zero address");
            require(spender != address(0), "ERC20: approve to the zero address");
    
            _allowances[owner][spender] = amount;
            emit Approval(owner, spender, amount);
        }
    
        /**
         * @dev Updates `owner` s allowance for `spender` based on spent `amount`.
         *
         * Does not update the allowance amount in case of infinite allowance.
         * Revert if not enough allowance is available.
         *
         * Might emit an {Approval} event.
         */
        function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
            uint256 currentAllowance = allowance(owner, spender);
            if (currentAllowance != type(uint256).max) {
                require(currentAllowance >= amount, "ERC20: insufficient allowance");
                unchecked {
                    _approve(owner, spender, currentAllowance - amount);
                }
            }
        }
    
        /**
         * @dev Hook that is called before any transfer of tokens. This includes
         * minting and burning.
         *
         * Calling conditions:
         *
         * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
         * will be transferred to `to`.
         * - when `from` is zero, `amount` tokens will be minted for `to`.
         * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
         * - `from` and `to` are never both zero.
         *
         * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
         */
        function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
    
        /**
         * @dev Hook that is called after any transfer of tokens. This includes
         * minting and burning.
         *
         * Calling conditions:
         *
         * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
         * has been transferred to `to`.
         * - when `from` is zero, `amount` tokens have been minted for `to`.
         * - when `to` is zero, `amount` of ``from``'s tokens have been burned.
         * - `from` and `to` are never both zero.
         *
         * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
         */
        function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
    }
    
    // File: contracts/erc20PublicMint.sol
    
    pragma solidity ^0.8.0;
    
    contract MyToken is ERC20 {
        constructor() ERC20("xkh666", "xkh") {
        }
        function mint(address to,uint256 num) public  {
            _mint(to, num);
        }
    }
    
  • 本地体验

    Remix - Ethereum IDE

    pragma solidity ^0.8.0;
    
    import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
    
    contract MyToken is ERC20 {
        constructor() ERC20("xkh666", "xkh") {
        }
        function mint(address to,uint256 num) public  {
            _mint(to, num);
        }
    }
    
  • 上链体验

  • 问题

    1. Gas消耗:Mint 99999 和 Mint 1e18 哪个消耗Gas多?为什么?
    2. 我可以转入到任意地址吗?会有什么风险?
    3. Mint不是一个标准的要求,那如何发现Mint的交易

协议应用层拓展

  • erc721代码

    pragma solidity ^0.8.0;
    
    import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    
    contract MyNFT is ERC721URIStorage {
        constructor(string memory _tokenName, string memory _tokenSymbol) 
            public ERC721(_tokenName, _tokenSymbol) {
        }
        //https://avatars.githubusercontent.com/u/13358272?v=4
        //https://ipfs.io/ipfs/bafybeieldy3ybghe5aqtby6y62jahtetizhaagycem4dzgwofmphlkzsde/1375.json
        function mint(address to, uint256 tokenId,string memory uri) public {
            _safeMint(to, tokenId);
            _setTokenURI(tokenId,uri);
        }
    }
    
  • 为什么有租赁的需要?

    1. 原生协议的局限:只能证明所有权,代扣与所有权趋同
    2. 市场应用的需求:分离NFT的资产价值和使用价值,释放出NFT的市场流动性
    3. 链上安全的威胁:盗签致使资产流失
  • 租赁的技术挑战

    1. 链上是无法信任的
    2. 租赁双方如何撮合?
    3. 信任依据为何?
    4. 历史协议的兼容性
    5. 世面应用的兼容性
  • ERC4907-无强制性的租赁声明

    • 如何设置

      function setUser(uint256 tokenId, address user, uint64 expires) public virtual{
        require(_isApprovedOrOwner(msg.sender, tokenId),"ERC721: transfer caller is not owner nor approved");
        UserInfo storage info =  _users[tokenId];//新增存储登记信息
        info.user = user;   
        info.expires = expires;
        emit UpdateUser(tokenId,user,expires); //发出事件通知链下应用
      }
      
    • 如何查询

      function userOf(uint256 tokenId)public view virtual returns(address){
        if( uint256(_users[tokenId].expires) >=  block.timestamp){
            return  _users[tokenId].user; 
        };//执行此函数,在未到期的情况下,返回此ID的当前用户地址
        else{return address(0);
        }//到期情况下,则返回0地址,意未占用
      }
      
    • 如何评价

      1. 无法向前兼容
      2. 原所有者权限极高,租赁者的维权能力薄弱(哪怕租赁期无法售卖和重置也好)
      3. Dapp应用层的兼容需增加识别方法
  • ERC5058-强制性的租赁声明

    请注意

    只有对NFT有持有权的地址,才可以给与别人锁定权。

    只有对NFT有锁定权的地址,才有权解锁该 NFT(或通过到期解锁)。

    锁定期可以产生(未实现)Bound NFT,来代表约束权

    • 给与锁定权

      function _lockApprove(address owner,address to,uint256 tokenId
          ) internal virtual {
              _lockApprovals[tokenId] = to;
              emit LockApproval(owner, to, tokenId);
          }
      
    • 执行锁定

      function _lock(
              address operator,
              uint256 tokenId,
              uint256 expired
          ) internal virtual {
              address owner = ERC721.ownerOf(tokenId);
      
              _beforeTokenLock(operator, owner, tokenId, expired);
      
              lockedTokens[tokenId] = expired;
              _lockApprovals[tokenId] = operator;
      
              emit Locked(operator, owner, tokenId, expired);
      
              _afterTokenLock(operator, owner, tokenId, expired);
          }
      
    • 如何评价

      总之:用户一部分权利给与一定信任,dapp也让渡一部分权益给与一定的服务。

      1. 锁定权难以定价,难以转化成应用场景。
      2. 流动性反而降低,Bound NFT本身无实物的交割权。

      什么是更合适的拓展?

      质(可取回,偏保管性质)、押(可变现,高利贷性质)。

      典(属官营,偏不动产)、当(属民营,偏可动产)。

      以上都是在期限内交换所有权、使用权,如果可以过期即具备强制转移,类似当铺思路则可以具有一定社会契约层牟利的动机。

  • ERC6147-半强制性的租赁声明

- 如何评价
    
    改进点:
    
    1. 减低4907的应用层兼容性要求,租赁给你,实际上是直接转给你,但自己还是守卫者(可以取回),但由于dapp更多使用owner,而不是userof,所以兼容性更好。
    2. 降低应用耦合难度的同时,更偏用户的管理权。
    
    局限性
    
    1. 时间点非强制性,致使售卖定价缺乏信任。如按挂牌价租1个月,但我可以反悔。
    2. 能清除授权的转移是非标准方法,为transferRemove,则opensea等无法兼容。
  • 综合点评

    挑战 4907 5058 6147
    概述 定义租赁角色,由所有者任意控制,到期自动解除关系 强制链上锁定,出卖转移权,仅可提前解除,用作质押类Defi场景。 定义守卫角色,所有者转为守卫,租赁方获得所有权,守卫可随时解除关系
    链上信任 非强制 强制 半强制
    定价撮合 无(需自研平台) 无(需自研平台) 中(借助os)
    信任依据 双方自觉 强行锁定 用户自觉
    往前兼容
    应用兼容 差(要识别租赁角色) 差(要识别锁定状态) 中(复用所有权)

    6551才是往前兼容好的典范:拓展阅读https://km.woa.com/articles/show/583323