DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transfer

admin 2025年4月14日09:13:23评论0 views字数 4337阅读14分27秒阅读模式
01
前言
此内容仅作为展示Solidity常见错误的概念证明。它严格用于教育目的,不应被解释为鼓励或认可任何形式的非法活动或实际的黑客攻击企图。所提供的信息仅供参考和学习,基于此内容采取的任何行动均由个人全权负责。使用这些信息应遵守适用的法律、法规和道德标准。
DeFiVulnLabs一共有47个漏洞实验,包括各种经典的合约漏洞和一些少见的可能造成安全问题的不安全代码,本系列将逐一解析每个漏洞,包括官方的解释和自己的理解。
02
转账收费代币不兼容
漏洞解析:
合约在处理某些具备通缩机制或者转账时会收取手续费的代币,可能会出现实际存入代币的实际于预期内的情况。
那么这个漏洞用什么用呢?由于代币实际接受数量未验证,所以可能用户可以超出存入大于自己本可存入的代币。
这个漏洞可以叫做代币实际接收数量未验证漏洞,或者 代币处理不当漏洞
代码解析:
VulnVault合约中,存在deposit 函数假设用户存入的代币数量amount参数一致。
如果 token 是具有通缩机制或转账时收取手续费的代币,实际存入的代币数量可能会少于 amount,导致 balances[msg.sender] 记录的数量与实际存入的数量不一致。

代码:DeFiVulnLabs/src/test/fee-on-transfer.sol

可以把代码分成三段,第一段代码是forge执行的代码测试流程
DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transfer
第二段代码是ERC20的接口规范
DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transfer
第三段代码是STA代币合约
DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transfer
然后重点查看STA ERC20代币里的重要部分

contract STA is ERC20Detailed {    using SafeMath for uint256;    mapping(address => uint256) private _balances;    mapping(address => mapping(address => uint256)) private _allowed;    string constant tokenName = "Statera";    string constant tokenSymbol = "STA";    uint8 constant tokenDecimals = 18;    uint256 _totalSupply = 100000000000000000000000000;    uint256 public basePercent = 100;    constructor()        public        payable        ERC20Detailed(tokenName, tokenSymbol, tokenDecimals)    {        _issue(msg.sender, _totalSupply);    }    function transfer(address to, uint256 value) public returns (bool) {        require(value <= _balances[msg.sender]);        require(to != address(0));        uint256 tokensToBurn = cut(value);        uint256 tokensToTransfer = value.sub(tokensToBurn);        _balances[msg.sender] = _balances[msg.sender].sub(value);        _balances[to] = _balances[to].add(tokensToTransfer);        _totalSupply = _totalSupply.sub(tokensToBurn);        emit Transfer(msg.sender, to, tokensToTransfer);        emit Transfer(msg.sender, address(0), tokensToBurn);        return true;    }    function cut(uint256 value) public view returns (uint256) {        uint256 roundValue = value.ceil(basePercent);        uint256 cutValue = roundValue.mul(basePercent).div(10000);        return cutValue;    }}

STA代币合约,存在cut函数,cut函数是计算手续费的,生成cutValue手续费值

uint256 cutValue = roundValue.mul(basePercent).div(10000);

然后在transfer里设置了一个tokenToBurn=cutValue,意思是每次转账都会收取1%的手续费,这个手续费会直接burn掉

emit Transfer(msg.sender, address(0), tokensToBurn);
第四段代码,VulnVault合约是一个存在漏洞的质押合约。正常情况下,用户可以通过 deposit 函数存入代币,并通过 withdraw 函数提取代币。
contract VulnVault {    mapping(address => uint256) private balances;    IERC20 private token;    constructor(address _tokenAddress) {        token = IERC20(_tokenAddress);    }    //存入代币    function deposit(uint256 amount) external {        require(amount > 0"Deposit amount must be greater than zero");        token.transferFrom(msg.senderaddress(this), amount);        balances[msg.sender] += amount;        emit Deposit(msg.sender, amount);    }    //提取代币    function withdraw(uint256 amount) external {        require(amount > 0"Withdrawal amount must be greater than zero");        require(amount <= balances[msg.sender], "Insufficient balance");        balances[msg.sender] -= amount;        token.transfer(msg.sender, amount);        emit Withdrawal(msg.sender, amount);    }    function getBalance(address account) external view returns (uint256) {        return balances[account];    }}
演示:
STA代币合约转账需要1%的手续费,如下所示,只要是调用STA代币的转账都会被吞掉1%的手续费。
DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transfer
1、假设Alice持有1000000个STA代币。
2、然后Alice将10000个STA代币存放入VulnVault合约中准备进行质押。
3、此时按照STA代币的规定,会收取1%的手续费,也就是100个STA代币被销毁
4、理论上VulnVault合约中应该余额为9900个代币,但是由于deposit增加的余额是balances[msg.sender] += amount,所以VulnVault实际上还是拥有10000个STA代币。
VulnVaultContract.getBalance(alice)   这里的alice在质押合约里的余额是10000,但是实际上应该是9900
这里就出现了问题,质押平台VlunVault合约没有扣除1%的手续费,相当于平台补贴了这1%的手续费,假设到时候要赎回,用户那边看的还是10000,就会损失平台的资金。

DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transfer

04
如何修复该问题
这是原本存在漏洞的方法deposit,可以看到原本的balances是直接从客户那里传入的值获取的
   function deposit(uint256 amount) external {        require(amount > 0"Deposit amount must be greater than zero");        token.transferFrom(msg.senderaddress(this), amount);        balances[msg.sender] += amount;        emit Deposit(msg.sender, amount);    }

而可以看到现在使用的是现在的用户余额的增加是依赖于

uint256 actualDepositAmount = balanceAfter - balanceBefore了。
function deposit(uint256 amount) external {        require(amount > 0"Deposit amount must be greater than zero");        //将amount        uint256 balanceBefore = token.balanceOf(address(this));        token.transferFrom(msg.sender, address(this), amount);        uint256 balanceAfter = token.balanceOf(address(this));        uint256 actualDepositAmount = balanceAfter - balanceBefore;        balances[msg.sender] += actualDepositAmount;        emit Deposit(msg.sender, actualDepositAmount);    }

也即效果应该如下,用户存入10000个代币进行质押,100的代币作为手续费burn掉,实际上存入的值应该是9900,而不是10000个DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transfer

原文始发于微信公众号(Ice ThirdSpace):DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transfer

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月14日09:13:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   DeFiVulnLabs靶场全系列详解(二十七)转账收费代币不兼容——fee-on-transferhttps://cn-sec.com/archives/3951162.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息