const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { max, min } = require('../../../helpers/math.js'); const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js'); const name = 'My Token'; const symbol = 'MTKN'; const initialSupply = 100n; async function fixture() { // this.accounts is used by shouldBehaveLikeERC20 const accounts = await ethers.getSigners(); const [holder, recipient, other] = accounts; const token = await ethers.deployContract('$ERC20TemporaryApproval', [name, symbol]); await token.$_mint(holder, initialSupply); const spender = await ethers.deployContract('$Address'); const batch = await ethers.deployContract('BatchCaller'); const getter = await ethers.deployContract('ERC20GetterHelper'); return { accounts, holder, recipient, other, token, spender, batch, getter }; } describe('ERC20TemporaryApproval', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); shouldBehaveLikeERC20(initialSupply); describe('setting and spending temporary allowance', function () { beforeEach(async function () { await this.token.connect(this.holder).transfer(this.batch, initialSupply); }); for (let { description, persistentAllowance, temporaryAllowance, amount, temporaryExpected, persistentExpected, } of [ { description: 'can set temporary allowance', temporaryAllowance: 42n }, { description: 'can set temporary allowance on top of persistent allowance', temporaryAllowance: 42n, persistentAllowance: 17n, }, { description: 'support allowance overflow', temporaryAllowance: ethers.MaxUint256, persistentAllowance: 17n }, { description: 'consuming temporary allowance alone', temporaryAllowance: 42n, amount: 2n }, { description: 'fallback to persistent allowance if temporary allowance is not sufficient', temporaryAllowance: 42n, persistentAllowance: 17n, amount: 50n, }, { description: 'do not reduce infinite temporary allowance #1', temporaryAllowance: ethers.MaxUint256, amount: 50n, temporaryExpected: ethers.MaxUint256, }, { description: 'do not reduce infinite temporary allowance #2', temporaryAllowance: 17n, persistentAllowance: ethers.MaxUint256, amount: 50n, temporaryExpected: ethers.MaxUint256, persistentExpected: ethers.MaxUint256, }, ]) { persistentAllowance ??= 0n; temporaryAllowance ??= 0n; amount ??= 0n; temporaryExpected ??= min(persistentAllowance + temporaryAllowance - amount, ethers.MaxUint256); persistentExpected ??= persistentAllowance - max(amount - temporaryAllowance, 0n); it(description, async function () { await expect( this.batch.execute( [ persistentAllowance && { target: this.token, value: 0n, data: this.token.interface.encodeFunctionData('approve', [this.spender.target, persistentAllowance]), }, temporaryAllowance && { target: this.token, value: 0n, data: this.token.interface.encodeFunctionData('temporaryApprove', [ this.spender.target, temporaryAllowance, ]), }, amount && { target: this.spender, value: 0n, data: this.spender.interface.encodeFunctionData('$functionCall', [ this.token.target, this.token.interface.encodeFunctionData('transferFrom', [ this.batch.target, this.recipient.address, amount, ]), ]), }, { target: this.getter, value: 0n, data: this.getter.interface.encodeFunctionData('allowance', [ this.token.target, this.batch.target, this.spender.target, ]), }, ].filter(Boolean), ), ) .to.emit(this.getter, 'ERC20Allowance') .withArgs(this.token, this.batch, this.spender, temporaryExpected); expect(await this.token.allowance(this.batch, this.spender)).to.equal(persistentExpected); }); } it('reverts when the recipient is the zero address', async function () { await expect(this.token.connect(this.holder).temporaryApprove(ethers.ZeroAddress, 1n)) .to.be.revertedWithCustomError(this.token, 'ERC20InvalidSpender') .withArgs(ethers.ZeroAddress); }); it('reverts when the token owner is the zero address', async function () { await expect(this.token.$_temporaryApprove(ethers.ZeroAddress, this.recipient, 1n)) .to.be.revertedWithCustomError(this.token, 'ERC20InvalidApprover') .withArgs(ethers.ZeroAddress); }); }); });