Skip to content

Commit 413cd0a

Browse files
authored
chore: add realistic USDT mock with approve-to-zero quirk (#26)
1 parent 4514f07 commit 413cd0a

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: BSD-3-Clause-Clear
2+
pragma solidity ^0.8.20;
3+
4+
import {ERC20Mock} from "./ERC20Mock.sol";
5+
6+
/**
7+
* @title USDTMock
8+
* @dev A more realistic USDT mock that replicates the real USDT's approve quirk:
9+
* to change a non-zero allowance, it must first be reset to 0.
10+
*/
11+
contract USDTMock is ERC20Mock {
12+
constructor() ERC20Mock("Tether USD (Mock)", "USDTMock", 6) {}
13+
14+
/**
15+
* @dev Overrides the approve function to require that the allowance is first reset to 0
16+
* before setting a new non-zero value. This replicates the real USDT behavior.
17+
*/
18+
function approve(address spender, uint256 value) public override returns (bool) {
19+
require(!(value != 0 && allowance(msg.sender, spender) != 0));
20+
return super.approve(spender, value);
21+
}
22+
}

contracts/confidential-token-wrappers-registry/tasks/mocks/deployMocks.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { task, types } from 'hardhat/config';
22
import { HardhatRuntimeEnvironment } from 'hardhat/types';
33

44
const ERC20_MOCK_CONTRACT_NAME = 'ERC20Mock';
5+
const USDT_MOCK_CONTRACT_NAME = 'USDTMock';
56

67
// Deploy the ERC20Mock contract
78
async function deployERC20Mock(hre: HardhatRuntimeEnvironment, name: string, symbol: string, decimals: number) {
@@ -38,6 +39,39 @@ async function deployERC20Mock(hre: HardhatRuntimeEnvironment, name: string, sym
3839
return contractAddress;
3940
}
4041

42+
// Deploy the USDTMock contract
43+
async function deployUSDTMock(hre: HardhatRuntimeEnvironment) {
44+
const { getNamedAccounts, ethers, deployments, network } = hre;
45+
const { save, getArtifact } = deployments;
46+
47+
const { deployer } = await getNamedAccounts();
48+
const deployerSigner = await ethers.getSigner(deployer);
49+
50+
const factory = await ethers.getContractFactory(USDT_MOCK_CONTRACT_NAME, deployerSigner);
51+
const contract = await factory.deploy();
52+
await contract.waitForDeployment();
53+
54+
const contractAddress = await contract.getAddress();
55+
56+
console.log(
57+
[
58+
`✅ Deployed USDTMock:`,
59+
` - Address: ${contractAddress}`,
60+
` - Deployed by deployer account: ${deployer}`,
61+
` - Network: ${network.name}`,
62+
'',
63+
].join('\n'),
64+
);
65+
66+
const artifact = await getArtifact(USDT_MOCK_CONTRACT_NAME);
67+
await save(USDT_MOCK_CONTRACT_NAME, {
68+
address: contractAddress,
69+
abi: artifact.abi,
70+
});
71+
72+
return contractAddress;
73+
}
74+
4175
// Deploy the ERC20Mock contract
4276
// Example usage:
4377
// npx hardhat task:deployERC20Mock --name "Mock Token" --symbol "MTK" --decimals 18 --network testnet
@@ -52,3 +86,14 @@ task('task:deployERC20Mock')
5286

5387
console.log('✅ ERC20Mock contract deployed\n');
5488
});
89+
90+
// Deploy the USDTMock contract (realistic USDT with "approve to 0 first" quirk)
91+
// Example usage:
92+
// npx hardhat task:deployUSDTMock --network testnet
93+
task('task:deployUSDTMock').setAction(async function (_, hre) {
94+
console.log('Deploying USDTMock contract...\n');
95+
96+
await deployUSDTMock(hre);
97+
98+
console.log('✅ USDTMock contract deployed\n');
99+
});

contracts/confidential-token-wrappers-registry/tasks/mocks/verify.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,19 @@ task('task:verifyMockERC20')
1818
});
1919
console.log(`Mock ERC20 contract verification complete\n`);
2020
});
21+
22+
// Verify the USDTMock contract
23+
// Example usage:
24+
// npx hardhat task:verifyUSDTMock --contract-address 0x1234567890123456789012345678901234567890 --network testnet
25+
task('task:verifyUSDTMock')
26+
.addParam('contractAddress', 'The address of the USDTMock contract to verify', '', types.string)
27+
.setAction(async function ({ contractAddress }, hre) {
28+
const { run } = hre;
29+
30+
console.log(`Verifying USDTMock contract at ${contractAddress}...\n`);
31+
await run('verify:verify', {
32+
address: contractAddress,
33+
constructorArguments: [],
34+
});
35+
console.log(`USDTMock contract verification complete\n`);
36+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { expect } from 'chai';
2+
import { ethers } from 'hardhat';
3+
4+
describe('ERC20Mock', function () {
5+
beforeEach(async function () {
6+
const [deployer, alice, bob] = await ethers.getSigners();
7+
const ERC20Mock = await ethers.getContractFactory('ERC20Mock');
8+
const token = await ERC20Mock.deploy('Mock Token', 'MTK', 18);
9+
await token.waitForDeployment();
10+
Object.assign(this, { token, deployer, alice, bob });
11+
});
12+
13+
it('should support custom decimals', async function () {
14+
const ERC20Mock = await ethers.getContractFactory('ERC20Mock');
15+
const token6 = await ERC20Mock.deploy('Six Decimals', 'SIX', 6);
16+
expect(await token6.decimals()).to.equal(6);
17+
});
18+
19+
it('should mint tokens', async function () {
20+
const amount = ethers.parseUnits('1000', 18);
21+
await this.token.mint(this.alice.address, amount);
22+
expect(await this.token.balanceOf(this.alice.address)).to.equal(amount);
23+
});
24+
25+
it('should revert if mint amount exceeds max', async function () {
26+
const maxAmount = ethers.parseUnits('1000000', 18);
27+
const tooMuch = maxAmount + 1n;
28+
await expect(this.token.mint(this.alice.address, tooMuch))
29+
.to.be.revertedWithCustomError(this.token, 'MintAmountExceedsMax')
30+
.withArgs(tooMuch, maxAmount);
31+
});
32+
});
33+
34+
describe('USDTMock', function () {
35+
beforeEach(async function () {
36+
const [deployer, alice, bob] = await ethers.getSigners();
37+
const USDTMock = await ethers.getContractFactory('USDTMock');
38+
const usdt = await USDTMock.deploy();
39+
await usdt.waitForDeployment();
40+
Object.assign(this, { usdt, deployer, alice, bob });
41+
});
42+
43+
it('should have correct name, symbol, and decimals', async function () {
44+
expect(await this.usdt.name()).to.equal('Tether USD (Mock)');
45+
expect(await this.usdt.symbol()).to.equal('USDTMock');
46+
expect(await this.usdt.decimals()).to.equal(6);
47+
});
48+
49+
it('should approve from zero allowance', async function () {
50+
const amount = ethers.parseUnits('100', 6);
51+
await this.usdt.connect(this.alice).approve(this.bob.address, amount);
52+
expect(await this.usdt.allowance(this.alice.address, this.bob.address)).to.equal(amount);
53+
});
54+
55+
it('should approve to zero', async function () {
56+
const amount = ethers.parseUnits('100', 6);
57+
await this.usdt.connect(this.alice).approve(this.bob.address, amount);
58+
await this.usdt.connect(this.alice).approve(this.bob.address, 0);
59+
expect(await this.usdt.allowance(this.alice.address, this.bob.address)).to.equal(0);
60+
});
61+
62+
it('should revert when changing non-zero allowance to non-zero', async function () {
63+
const amount1 = ethers.parseUnits('100', 6);
64+
const amount2 = ethers.parseUnits('200', 6);
65+
await this.usdt.connect(this.alice).approve(this.bob.address, amount1);
66+
await expect(this.usdt.connect(this.alice).approve(this.bob.address, amount2)).to.be.revertedWithoutReason();
67+
});
68+
69+
it('should allow setting new allowance after resetting to zero', async function () {
70+
const amount1 = ethers.parseUnits('100', 6);
71+
const amount2 = ethers.parseUnits('200', 6);
72+
await this.usdt.connect(this.alice).approve(this.bob.address, amount1);
73+
await this.usdt.connect(this.alice).approve(this.bob.address, 0);
74+
await this.usdt.connect(this.alice).approve(this.bob.address, amount2);
75+
expect(await this.usdt.allowance(this.alice.address, this.bob.address)).to.equal(amount2);
76+
});
77+
});

0 commit comments

Comments
 (0)