|
@@ -0,0 +1,142 @@
|
|
|
+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);
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|