Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion contracts/interfaces/IERC2612.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
pragma solidity ^0.8.35;

import { _IERC2612 } from './_IERC2612.sol';
import { IERC20 } from './IERC20.sol';

/**
* @title ERC2612 interface
* @dev see https://eips.ethereum.org/EIPS/eip-2612.
*/
interface IERC2612 is _IERC2612 {
interface IERC2612 is _IERC2612, IERC20 {
/**
* @notice return the EIP-712 domain separator unique to contract and chain
* @return domainSeparator domain separator
Expand Down
4 changes: 3 additions & 1 deletion contracts/interfaces/_IERC2612.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

pragma solidity ^0.8.35;

interface _IERC2612 {}
import { _IERC20 } from './_IERC20.sol';

interface _IERC2612 is _IERC20 {}
6 changes: 5 additions & 1 deletion contracts/token/fungible/permit/FungibleTokenPermit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity ^0.8.35;

import { IERC2612 } from '../../../interfaces/IERC2612.sol';
import { IERC5267 } from '../../../interfaces/IERC5267.sol';
import { FungibleTokenMetadata } from '../metadata/FungibleTokenMetadata.sol';
import { FungibleToken } from '../FungibleToken.sol';
import { _FungibleTokenPermit } from './_FungibleTokenPermit.sol';
import { IFungibleTokenPermit } from './IFungibleTokenPermit.sol';

Expand All @@ -12,7 +14,9 @@ import { IFungibleTokenPermit } from './IFungibleTokenPermit.sol';
*/
abstract contract FungibleTokenPermit is
IFungibleTokenPermit,
_FungibleTokenPermit
_FungibleTokenPermit,
FungibleToken,
FungibleTokenMetadata
{
/**
* @inheritdoc IERC2612
Expand Down
10 changes: 9 additions & 1 deletion contracts/token/fungible/permit/IFungibleTokenPermit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ pragma solidity ^0.8.35;

import { IERC2612 } from '../../../interfaces/IERC2612.sol';
import { IERC5267 } from '../../../interfaces/IERC5267.sol';
import { IFungibleTokenMetadata } from '../metadata/IFungibleTokenMetadata.sol';
import { IFungibleToken } from '../IFungibleToken.sol';
import { _IFungibleTokenPermit } from './_IFungibleTokenPermit.sol';

interface IFungibleTokenPermit is _IFungibleTokenPermit, IERC2612, IERC5267 {}
interface IFungibleTokenPermit is
_IFungibleTokenPermit,
IFungibleToken,
IFungibleTokenMetadata,
IERC2612,
IERC5267
{}
46 changes: 46 additions & 0 deletions contracts/utils/SafeERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pragma solidity ^0.8.35;

import { IERC20 } from '../interfaces/IERC20.sol';
import { IERC2612 } from '../interfaces/IERC2612.sol';
import { Address } from './Address.sol';

/**
Expand All @@ -15,6 +16,7 @@ library SafeERC20 {
error SafeERC20__ApproveFromNonZeroToNonZero();
error SafeERC20__DecreaseAllowanceBelowZero();
error SafeERC20__OperationFailed();
error SafeERC20__PermitFailed();

function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(
Expand Down Expand Up @@ -83,6 +85,50 @@ library SafeERC20 {
}
}

/**
* @notice approve spender to transfer tokens held by owner via ERC-2612 permit signature
* @dev tolerates reverts so that a front-run or already-executed permit does not block
* the caller. If the permit call reverts, the existing allowance is checked; execution
* continues only if the spender already has sufficient allowance.
* @param token ERC-2612 permit token interface
* @param owner holder of tokens and signer of permit
* @param spender beneficiary of approval
* @param value quantity of tokens to approve
* @param deadline timestamp after which permit is invalid
* @param v secp256k1 'v' value
* @param r secp256k1 'r' value
* @param s secp256k1 's' value
*/
function safePermit(
IERC2612 token,
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
(bool success, ) = address(token).call(
abi.encodeWithSelector(
token.permit.selector,
owner,
spender,
value,
deadline,
v,
r,
s
)
);

// permit authorizes an exact amount; if the call reverted, the existing
// allowance must equal the signed value for the permit to be considered
// already executed (e.g., via front-running)
if (!success && token.allowance(owner, spender) != value)
revert SafeERC20__PermitFailed();
}

/**
* @notice send transaction data and check validity of return value, if present
* @param token ERC20 token interface
Expand Down
254 changes: 254 additions & 0 deletions test/utils/SafeERC20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import { deployMockContract } from '@solidstate/library';
import { $SafeERC20, $SafeERC20__factory } from '@solidstate/typechain-types';
import { expect } from 'chai';
import { ethers } from 'hardhat';

describe('SafeERC20', () => {
let instance: $SafeERC20;
let deployer: SignerWithAddress;
let receiver: SignerWithAddress;
let spender: SignerWithAddress;

beforeEach(async () => {
[deployer, receiver, spender] = await ethers.getSigners();
instance = await new $SafeERC20__factory(deployer).deploy();
});

describe('#safeTransfer', () => {
it('transfers tokens', async () => {
const mock = await deployMockContract(deployer, [
'function transfer(address,uint256) external returns (bool)',
]);

await mock.mock.transfer.withArgs(receiver.address, 1n).returns(true);

await expect(instance.$safeTransfer(mock.address, receiver.address, 1n))
.not.to.be.reverted;
});

describe('reverts if', () => {
it('transfer returns false', async () => {
const mock = await deployMockContract(deployer, [
'function transfer(address,uint256) external returns (bool)',
]);

await mock.mock.transfer.withArgs(receiver.address, 1n).returns(false);

await expect(
instance.$safeTransfer(mock.address, receiver.address, 1n),
).to.be.revertedWithCustomError(instance, 'SafeERC20__OperationFailed');
});

it('transfer reverts', async () => {
const mock = await deployMockContract(deployer, [
'function transfer(address,uint256) external returns (bool)',
]);

await mock.mock.transfer
.withArgs(receiver.address, 1n)
.revertsWithReason('TRANSFER_FAILED');

await expect(
instance.$safeTransfer(mock.address, receiver.address, 1n),
).to.be.revertedWith('TRANSFER_FAILED');
});
});
});

describe('#safeTransferFrom', () => {
it('transfers tokens', async () => {
const mock = await deployMockContract(deployer, [
'function transferFrom(address,address,uint256) external returns (bool)',
]);

await mock.mock.transferFrom
.withArgs(deployer.address, receiver.address, 1n)
.returns(true);

await expect(
instance.$safeTransferFrom(
mock.address,
deployer.address,
receiver.address,
1n,
),
).not.to.be.reverted;
});

describe('reverts if', () => {
it('transferFrom returns false', async () => {
const mock = await deployMockContract(deployer, [
'function transferFrom(address,address,uint256) external returns (bool)',
]);

await mock.mock.transferFrom
.withArgs(deployer.address, receiver.address, 1n)
.returns(false);

await expect(
instance.$safeTransferFrom(
mock.address,
deployer.address,
receiver.address,
1n,
),
).to.be.revertedWithCustomError(instance, 'SafeERC20__OperationFailed');
});

it('transferFrom reverts', async () => {
const mock = await deployMockContract(deployer, [
'function transferFrom(address,address,uint256) external returns (bool)',
]);

await mock.mock.transferFrom
.withArgs(deployer.address, receiver.address, 1n)
.revertsWithReason('TRANSFER_FROM_FAILED');

await expect(
instance.$safeTransferFrom(
mock.address,
deployer.address,
receiver.address,
1n,
),
).to.be.revertedWith('TRANSFER_FROM_FAILED');
});
});
});

describe('#safeApprove', () => {
it('approves spender', async () => {
const mock = await deployMockContract(deployer, [
'function approve(address,uint256) external returns (bool)',
]);

await mock.mock.approve.withArgs(spender.address, 1n).returns(true);

await expect(instance.$safeApprove(mock.address, spender.address, 1n)).not
.to.be.reverted;
});

describe('reverts if', () => {
it('approve returns false', async () => {
const mock = await deployMockContract(deployer, [
'function approve(address,uint256) external returns (bool)',
]);

await mock.mock.approve.withArgs(spender.address, 1n).returns(false);

await expect(
instance.$safeApprove(mock.address, spender.address, 1n),
).to.be.revertedWithCustomError(instance, 'SafeERC20__OperationFailed');
});
});
});

describe('#safeIncreaseAllowance', () => {
it('todo');
});

describe('#safeDecreaseAllowance', () => {
it('todo');
});

describe('#safePermit', () => {
it('executes permit', async () => {
const mock = await deployMockContract(deployer, [
'function permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external',
]);

await mock.mock.permit.returns();

await expect(
instance.$safePermit(
mock.address,
deployer.address,
spender.address,
1n,
ethers.MaxUint256,
27,
ethers.ZeroHash,
ethers.ZeroHash,
),
).not.to.be.reverted;
});

it('tolerates front-run', async () => {
const mock = await deployMockContract(deployer, [
'function permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external',
'function allowance(address,address) external view returns (uint256)',
]);

await mock.mock.permit.reverts();
await mock.mock.allowance
.withArgs(deployer.address, spender.address)
.returns(1n);

await expect(
instance.$safePermit(
mock.address,
deployer.address,
spender.address,
1n,
ethers.MaxUint256,
27,
ethers.ZeroHash,
ethers.ZeroHash,
),
).not.to.be.reverted;
});

describe('reverts if', () => {
it('permit fails and allowance is insufficient', async () => {
const mock = await deployMockContract(deployer, [
'function permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external',
'function allowance(address,address) external view returns (uint256)',
]);

await mock.mock.permit.reverts();
await mock.mock.allowance
.withArgs(deployer.address, spender.address)
.returns(0n);

await expect(
instance.$safePermit(
mock.address,
deployer.address,
spender.address,
1n,
ethers.MaxUint256,
27,
ethers.ZeroHash,
ethers.ZeroHash,
),
).to.be.revertedWithCustomError(instance, 'SafeERC20__PermitFailed');
});

it('permit fails and allowance is excessive', async () => {
const mock = await deployMockContract(deployer, [
'function permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external',
'function allowance(address,address) external view returns (uint256)',
]);

await mock.mock.permit.reverts();
await mock.mock.allowance
.withArgs(deployer.address, spender.address)
.returns(2n);

await expect(
instance.$safePermit(
mock.address,
deployer.address,
spender.address,
1n,
ethers.MaxUint256,
27,
ethers.ZeroHash,
ethers.ZeroHash,
),
).to.be.revertedWithCustomError(instance, 'SafeERC20__PermitFailed');
});
});
});
});