Skip to content

Commit

Permalink
Max approval erc20s (#971)
Browse files Browse the repository at this point in the history
Co-authored-by: Akshat Mittal <[email protected]>
Co-authored-by: Patrick McKelvy <[email protected]>
  • Loading branch information
3 people committed Oct 11, 2023
1 parent fccf5de commit 7affc85
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 8 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

# 3.0.1

### Upgrade steps

Update `BackingManager`, both `RevenueTraders` (rTokenTrader/rsrTrader), and call `Broker.setBatchTradeImplementation()` passing in the new `GnosisTrade` address.

# 3.0.0

### Upgrade Steps
Expand Down
43 changes: 43 additions & 0 deletions contracts/libraries/Allowance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

interface IERC20ApproveOnly {
function approve(address spender, uint256 value) external;

function allowance(address owner, address spender) external view returns (uint256);
}

library AllowanceLib {
/// An approve helper that:
/// 1. Sets initial allowance to 0
/// 2. Tries to set the provided allowance
/// 3. Falls back to setting a maximum allowance, if (2) fails
/// Context: Some new-age ERC20s think it's a good idea to revert for allowances
/// that are > 0 but < type(uint256).max.
function safeApproveFallbackToMax(
address tokenAddress,
address spender,
uint256 value
) internal {
IERC20ApproveOnly token = IERC20ApproveOnly(tokenAddress);

// 1. Set initial allowance to 0
token.approve(spender, 0);
require(token.allowance(address(this), spender) == 0, "allowance not 0");

if (value == 0) return;

// 2. Try to set the provided allowance
bool success; // bool success = false;
try token.approve(spender, value) {
success = token.allowance(address(this), spender) == value;
// solhint-disable-next-line no-empty-blocks
} catch {}

// 3. Fall-back to setting a maximum allowance
if (!success) {
token.approve(spender, type(uint256).max);
require(token.allowance(address(this), spender) >= value, "allowance missing");
}
}
}
3 changes: 3 additions & 0 deletions contracts/p0/RevenueTrader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ contract RevenueTraderP0 is TradingP0, IRevenueTrader {
uint256 bal = tokenToBuy.balanceOf(address(this));
tokenToBuy.safeApprove(address(main.distributor()), 0);
tokenToBuy.safeApprove(address(main.distributor()), bal);
// do not need to use AllowanceLib.safeApproveFallbackToCustom here because
// tokenToBuy can be assumed to be either RSR or the RToken

main.distributor().distribute(tokenToBuy, bal);
}
}
13 changes: 11 additions & 2 deletions contracts/p0/mixins/Trading.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../../interfaces/IBroker.sol";
import "../../interfaces/IMain.sol";
import "../../interfaces/ITrade.sol";
import "../../libraries/Allowance.sol";
import "../../libraries/Fixed.sol";
import "./Rewardable.sol";

Expand Down Expand Up @@ -68,8 +69,16 @@ abstract contract TradingP0 is RewardableP0, ITrading {
IBroker broker = main.broker();
assert(address(trades[req.sell.erc20()]) == address(0));

req.sell.erc20().safeApprove(address(broker), 0);
req.sell.erc20().safeApprove(address(broker), req.sellAmount);
// Set allowance via custom approval -- first sets allowance to 0, then sets allowance
// to either the requested amount or the maximum possible amount, if that fails.
//
// Context: wcUSDCv3 has a non-standard approve() function that reverts if the approve
// amount is > 0 and < type(uint256).max.
AllowanceLib.safeApproveFallbackToMax(
address(req.sell.erc20()),
address(broker),
req.sellAmount
);

trade = broker.openTrade(kind, req, prices);
trades[req.sell.erc20()] = trade;
Expand Down
3 changes: 3 additions & 0 deletions contracts/p1/RevenueTrader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader {
uint256 bal = tokenToBuy.balanceOf(address(this));
tokenToBuy.safeApprove(address(distributor), 0);
tokenToBuy.safeApprove(address(distributor), bal);

// do not need to use AllowanceLib.safeApproveFallbackToCustom here because
// tokenToBuy can be assumed to be either RSR or the RToken
distributor.distribute(tokenToBuy, bal);
}

Expand Down
9 changes: 7 additions & 2 deletions contracts/p1/mixins/Trading.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Multicall.sol";
import "../../interfaces/ITrade.sol";
import "../../interfaces/ITrading.sol";
import "../../libraries/Allowance.sol";
import "../../libraries/Fixed.sol";
import "./Component.sol";
import "./RewardableLib.sol";
Expand Down Expand Up @@ -120,8 +121,12 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl
IERC20 sell = req.sell.erc20();
assert(address(trades[sell]) == address(0));

IERC20Upgradeable(address(sell)).safeApprove(address(broker), 0);
IERC20Upgradeable(address(sell)).safeApprove(address(broker), req.sellAmount);
// Set allowance via custom approval -- first sets allowance to 0, then sets allowance
// to either the requested amount or the maximum possible amount, if that fails.
//
// Context: wcUSDCv3 has a non-standard approve() function that reverts if the approve
// amount is > 0 and < type(uint256).max.
AllowanceLib.safeApproveFallbackToMax(address(sell), address(broker), req.sellAmount);

trade = broker.openTrade(kind, req, prices);
trades[sell] = trade;
Expand Down
10 changes: 10 additions & 0 deletions contracts/plugins/mocks/BadERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ contract BadERC20 is ERC20Mock {
uint8 private _decimals;
uint192 public transferFee; // {1}
bool public revertDecimals;
bool public revertApprove; // if true, reverts for any approve > 0 and < type(uint256).max

mapping(address => bool) public censored;

Expand All @@ -34,6 +35,10 @@ contract BadERC20 is ERC20Mock {
censored[account] = val;
}

function setRevertApprove(bool newRevertApprove) external {
revertApprove = newRevertApprove;
}

function decimals() public view override returns (uint8) {
bytes memory data = abi.encodePacked((bytes4(keccak256("absentDecimalsFn()"))));

Expand All @@ -42,6 +47,11 @@ contract BadERC20 is ERC20Mock {
return _decimals;
}

function approve(address spender, uint256 amount) public virtual override returns (bool) {
if (revertApprove && amount > 0 && amount < type(uint256).max) revert("revertApprove");
return super.approve(spender, amount);
}

function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
if (censored[owner] || censored[to]) revert("censored");
Expand Down
10 changes: 7 additions & 3 deletions contracts/plugins/trading/GnosisTrade.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import "../../libraries/Allowance.sol";
import "../../libraries/Fixed.sol";
import "../../interfaces/IBroker.sol";
import "../../interfaces/IGnosis.sol";
Expand Down Expand Up @@ -130,9 +131,12 @@ contract GnosisTrade is ITrade {

// == Interactions ==

// Set allowance (two safeApprove calls to support USDT)
IERC20Upgradeable(address(sell)).safeApprove(address(gnosis), 0);
IERC20Upgradeable(address(sell)).safeApprove(address(gnosis), initBal);
// Set allowance via custom approval -- first sets allowance to 0, then sets allowance
// to either the requested amount or the maximum possible amount, if that fails.
//
// Context: wcUSDCv3 has a non-standard approve() function that reverts if the approve
// amount is > 0 and < type(uint256).max.
AllowanceLib.safeApproveFallbackToMax(address(sell), address(gnosis), initBal);

auctionId = gnosis.initiateAuction(
sell,
Expand Down
2 changes: 1 addition & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const config: HardhatUserConfig = {
// network for tests/in-process stuff
forking: useEnv('FORK')
? {
url: forkRpcs[(useEnv('FORK_NETWORK') ?? 'mainnet') as Network],
url: forkRpcs[useEnv('FORK_NETWORK', 'mainnet') as Network],
blockNumber: Number(useEnv(`FORK_BLOCK`, forkBlockNumber['default'].toString())),
}
: undefined,
Expand Down
35 changes: 35 additions & 0 deletions test/scenario/BadERC20.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,4 +396,39 @@ describe(`Bad ERC20 - P${IMPLEMENTATION}`, () => {
)
})
})

describe('with fussy approvals', function () {
let issueAmt: BigNumber

beforeEach(async () => {
issueAmt = initialBal.div(100)
await token0.connect(addr1).approve(rToken.address, issueAmt)
await token0.setRevertApprove(true)
await rToken.connect(addr1).issue(issueAmt)
})

context('Regression tests wcUSDCv3 10/10/2023', () => {
it('should not revert during recollateralization', async () => {
await basketHandler.setPrimeBasket(
[token0.address, backupToken.address],
[fp('0.5'), fp('0.5')]
)
await basketHandler.refreshBasket()

// Should launch recollateralization auction successfully
await expect(backingManager.rebalance(TradeKind.BATCH_AUCTION))
.to.emit(backingManager, 'TradeStarted')
.withArgs(anyValue, token0.address, backupToken.address, anyValue, anyValue)
})

it('should not revert during revenue auction', async () => {
await token0.mint(rsrTrader.address, issueAmt)

// Should launch revenue auction successfully
await expect(rsrTrader.manageTokens([token0.address], [TradeKind.BATCH_AUCTION]))
.to.emit(rsrTrader, 'TradeStarted')
.withArgs(anyValue, token0.address, rsr.address, anyValue, anyValue)
})
})
})
})

0 comments on commit 7affc85

Please sign in to comment.