Как использовать Transfer и TransferFrom в других smart-contract для перевода Tether, если используется модификатор onlyPayloadSize?
Ошибка при вызове transferFrom
Требовалось написать контракт, который будет принимать от юзеров USDT в сети Ethereum и в дальнейшем выводить их по запросам тех же пользователей или owner'a.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TransferUSDT is Ownable {
IERC20 private immutable USDTContract;
mapping(address => uint) private balances;
constructor(address _usdt) {
USDTContract = IERC20(_usdt);
}
function deposit(uint _amount) external {
require(getAllowance() >= _amount);
USDTContract.transferFrom(msg.sender, address(this), _amount);
balances[msg.sender] += _amount;
}
...
}После аппрува токенов USDT для возможности списания их с адерса юзера в пользу контракта TransferUSDT, я вызвал функцию deposit и получил ошибку:
Неизвестный модификатор
??? В поисках ответа я полез в контракт USDT и обнаружил не обычную функцию transferFrom:
// Forward ERC20 methods to upgraded contract if this one is deprecated
function transferFrom(address _from, address _to, uint _value) public whenNotPaused {
require(!isBlackListed[_from]);
if (deprecated) {
return UpgradedStandardToken(upgradedAddress).transferFromByLegacy(msg.sender, _from, _to, _value);
} else {
return super.transferFrom(_from, _to, _value);
}
}При положении deprecated = false, функция обращается к реализации transferFrom в родительском контракте:
contract StandardToken is BasicToken, ERC20 {
mapping (address => mapping (address => uint)) public allowed;
uint public constant MAX_UINT = 2**256 - 1;
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint the amount of tokens to be transferred
*/
function transferFrom(address _from, address _to, uint _value) public onlyPayloadSize(3 * 32) {
var _allowance = allowed[_from][msg.sender];
// Check is not needed because sub(_allowance, _value) will already throw if this condition is not met
// if (_value > _allowance) throw;
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
if (_allowance < MAX_UINT) {
allowed[_from][msg.sender] = _allowance.sub(_value);
}
uint sendAmount = _value.sub(fee);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
Transfer(_from, owner, fee);
}
Transfer(_from, _to, sendAmount);
}где сразу обращаешь внимание на модификатор onlyPayloadSize(3 * 32).
modifier onlyPayloadSize(uint size) {
require(!(msg.data.length < size + 4));
_;
}Назначение модификатора
Модификатор проверяет размер data отправляемой в транзакции. Это ограничение на вызов функции из смарт-контракта и никак не влияет на перевод с обычных кошельков.
Вызывающий контракт может воспользоваться атакой короткого адреса.
Атака по короткому адресу — это когда контракт получает меньше данных, чем ожидалось, и Solidity заполняет недостающие байты нулями. Развернутый смарт-контракт не может предотвратить это и интерпретирует эти лишние нули как часть правильного значения, провоцируя серьезные проблемы.
Если атакуемый сгенерирует адрес типа 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000. он может предоставить 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(нули опущены) передаточной функции. Это фактически вызовет передачу значения, сдвинутого на 16 бит, то есть в 65536 раз больше, чем X, на учетную запись Ethereum атакующего.
msg.dataбудет выглядеть следующим образом :
Function: transfer(address _to, uint256 _value) 0xa9059cbb0000000000000000000000000797350000000000000000000000000000000000000000000005150ac4c39a6f3f0000
Таким образом в нашем случае размер data не должен превышать
3 х 32 byte + 4 byte = 100 byte ,
что соответствует 3м слотам по 32 байта под 3 аргумента, передаваемых в функции и 4 байта селектор функции.
В момент передачи контракт дополняет msg.data до 128 байт, что в итоге приводит к ошибке.
Как обойти модификатор
Для обхода этого модификатора можно использовать библиотеку OpenZeppelinSafeERC20.sol. По примеру, Aave:
function transferToFeeCollectionAddress(
address _token,
address _user,
uint256 _amount,
address _destination
) external payable onlyLendingPool {
address payable feeAddress = address(uint160(_destination)); //cast the address to payable
if (_token != EthAddressLib.ethAddress()) {
require(
msg.value == 0,
"User is sending ETH along with the ERC20 transfer. Check the value attribute of the transaction"
);
ERC20(_token).safeTransferFrom(_user, feeAddress, _amount);
} else {
require(msg.value >= _amount, "The amount and the value sent to deposit do not match");
//solium-disable-next-line
(bool result, ) = feeAddress.call.value(_amount).gas(50000)("");
require(result, "Transfer of ETH failed");
}
}Тогда наш код будет выглядеть так:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "openzeppelin-solidity/contracts/token/ERC20/utils/SafeERC20.sol";
contract TransferUSDT is Ownable {
using SafeERC20 for IERC20;
IERC20 private immutable usdt;
mapping (address => uint) private balances;
constructor(address _usdt) {
usdt = IERC20(_usdt);
}
function deposit(uint _amount) external {
require(getAllawance() > _amount, "Not approved!");
usdt.safeTransferFrom(msg.sender, address(this), _amount);
}
....
}safeTransfer передает аргументы через abi.encodeWithSelector(token.transferFrom.selector, from, to, value),
что потребляет немного больше газа, но для решения нашей задачи подходит.
Итог
Ряд аудиторских компаний предлагали отказаться от использования проверки на размер data в контракте и делать это на бирже. До сих пор никто не нашел способа воспользоваться атакой короткого адреса и это вызывает только некоторые проблемы.