Skip to content

Commit

Permalink
Add unified interface for EOA and contract wallet signatures
Browse files Browse the repository at this point in the history
* Add unified interface for EOA and contract wallet signatures

* Modify comment
  • Loading branch information
yvonnezhangc authored and circle-aloychan committed Oct 19, 2023
1 parent 8332ba5 commit ad75174
Show file tree
Hide file tree
Showing 12 changed files with 610 additions and 257 deletions.
17 changes: 17 additions & 0 deletions @types/AnyFiatTokenV2Instance.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
FiatTokenV2Instance,
FiatTokenV21Instance,
FiatTokenV22Instance,
} from "./generated";

export interface FiatTokenV22InstanceExtended extends FiatTokenV22Instance {
permit?: typeof FiatTokenV2Instance.permit;
transferWithAuthorization?: typeof FiatTokenV2Instance.transferWithAuthorization;
receiveWithAuthorization?: typeof FiatTokenV2Instance.receiveWithAuthorization;
cancelAuthorization?: typeof FiatTokenV2Instance.cancelAuthorization;
}

export type AnyFiatTokenV2Instance =
| FiatTokenV2Instance
| FiatTokenV21Instance
| FiatTokenV22InstanceExtended;
99 changes: 99 additions & 0 deletions contracts/v2/FiatTokenV2_2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,103 @@ contract FiatTokenV2_2 is FiatTokenV2_1 {
function _domainSeparator() internal override view returns (bytes32) {
return EIP712.makeDomainSeparator(name, "2", _chainId());
}

/**
* @notice Update allowance with a signed permit
* @dev EOA wallet signatures should be packed in the order of r, s, v.
* @param owner Token owner's address (Authorizer)
* @param spender Spender's address
* @param value Amount of allowance
* @param deadline Expiration time, seconds since the epoch
* @param signature Signature bytes signed by an EOA wallet or a contract wallet
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
bytes memory signature
) external whenNotPaused notBlacklisted(owner) notBlacklisted(spender) {
_permit(owner, spender, value, deadline, signature);
}

/**
* @notice Execute a transfer with a signed authorization
* @dev EOA wallet signatures should be packed in the order of r, s, v.
* @param from Payer's address (Authorizer)
* @param to Payee's address
* @param value Amount to be transferred
* @param validAfter The time after which this is valid (unix time)
* @param validBefore The time before which this is valid (unix time)
* @param nonce Unique nonce
* @param signature Signature bytes signed by an EOA wallet or a contract wallet
*/
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) external whenNotPaused notBlacklisted(from) notBlacklisted(to) {
_transferWithAuthorization(
from,
to,
value,
validAfter,
validBefore,
nonce,
signature
);
}

/**
* @notice Receive a transfer with a signed authorization from the payer
* @dev This has an additional check to ensure that the payee's address
* matches the caller of this function to prevent front-running attacks.
* EOA wallet signatures should be packed in the order of r, s, v.
* @param from Payer's address (Authorizer)
* @param to Payee's address
* @param value Amount to be transferred
* @param validAfter The time after which this is valid (unix time)
* @param validBefore The time before which this is valid (unix time)
* @param nonce Unique nonce
* @param signature Signature bytes signed by an EOA wallet or a contract wallet
*/
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) external whenNotPaused notBlacklisted(from) notBlacklisted(to) {
_receiveWithAuthorization(
from,
to,
value,
validAfter,
validBefore,
nonce,
signature
);
}

/**
* @notice Attempt to cancel an authorization
* @dev Works only if the authorization is not yet used.
* EOA wallet signatures should be packed in the order of r, s, v.
* @param authorizer Authorizer's address
* @param nonce Nonce of the authorization
* @param signature Signature bytes signed by an EOA wallet or a contract wallet
*/
function cancelAuthorization(
address authorizer,
bytes32 nonce,
bytes memory signature
) external whenNotPaused {
_cancelAuthorization(authorizer, nonce, signature);
}
}
39 changes: 32 additions & 7 deletions test/v2/FiatTokenV2.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { behavesLikeRescuable } from "../v1.1/Rescuable.behavior";
import { FiatTokenV2Instance, RescuableInstance } from "../../@types/generated";
import {
MockErc1271WalletInstance,
FiatTokenV2Instance,
RescuableInstance,
} from "../../@types/generated";
import { AnyFiatTokenV2Instance } from "../../@types/AnyFiatTokenV2Instance";
import { usesOriginalStorageSlotPositions } from "../helpers/storageSlots.behavior";
import { hasSafeAllowance } from "./safeAllowance.behavior";
import { hasGasAbstraction } from "./GasAbstraction/GasAbstraction.behavior";
import { makeDomainSeparator } from "./GasAbstraction/helpers";
import {
SignatureBytesType,
TestParams,
WalletType,
makeDomainSeparator,
} from "./GasAbstraction/helpers";
import { expectRevert } from "../helpers";
import { testTransferWithMultipleAuthorizations } from "./GasAbstraction/testTransferWithMultipleAuthorizations";

const FiatTokenV2 = artifacts.require("FiatTokenV2");
const MockERC1271Wallet = artifacts.require("MockERC1271Wallet");

contract("FiatTokenV2", (accounts) => {
const fiatTokenOwner = accounts[9];
Expand All @@ -32,7 +44,7 @@ contract("FiatTokenV2", (accounts) => {

export function behavesLikeFiatTokenV2(
accounts: Truffle.Accounts,
getFiatToken: () => FiatTokenV2Instance,
getFiatToken: () => AnyFiatTokenV2Instance,
fiatTokenOwner: string
): void {
let domainSeparator: string;
Expand Down Expand Up @@ -60,12 +72,19 @@ export function behavesLikeFiatTokenV2(

hasSafeAllowance(getFiatToken, fiatTokenOwner, accounts);

hasGasAbstraction(
const testParams: TestParams = {
getFiatToken,
() => domainSeparator,
getDomainSeparator: () => domainSeparator,
getERC1271Wallet,
fiatTokenOwner,
accounts
);
accounts,
signerWalletType: WalletType.EOA,
signatureBytesType: SignatureBytesType.Unpacked,
};

hasGasAbstraction(testParams);

testTransferWithMultipleAuthorizations(testParams);

it("disallows calling initializeV2 twice", async () => {
// It was called once in beforeEach. Try to call again.
Expand All @@ -74,3 +93,9 @@ export function behavesLikeFiatTokenV2(
);
});
}

export async function getERC1271Wallet(
owner: string
): Promise<MockErc1271WalletInstance> {
return await MockERC1271Wallet.new(owner);
}
128 changes: 121 additions & 7 deletions test/v2/FiatTokenV2_2.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,138 @@
import { FiatTokenV22Instance } from "../../@types/generated";
import {
AnyFiatTokenV2Instance,
FiatTokenV22InstanceExtended,
} from "../../@types/AnyFiatTokenV2Instance";
import { expectRevert, initializeToVersion } from "../helpers";
import { behavesLikeFiatTokenV2 } from "./FiatTokenV2.test";
import { behavesLikeFiatTokenV2, getERC1271Wallet } from "./FiatTokenV2.test";
import { hasGasAbstraction } from "./GasAbstraction/GasAbstraction.behavior";
import {
SignatureBytesType,
WalletType,
makeDomainSeparator,
permitSignature,
permitSignatureV22,
transferWithAuthorizationSignature,
transferWithAuthorizationSignatureV22,
cancelAuthorizationSignature,
cancelAuthorizationSignatureV22,
receiveWithAuthorizationSignature,
receiveWithAuthorizationSignatureV22,
} from "./GasAbstraction/helpers";

const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2");

contract("FiatTokenV2_2", (accounts) => {
const fiatTokenOwner = accounts[9];
const [, , lostAndFound] = accounts;
let fiatToken: FiatTokenV22Instance;
let fiatToken: FiatTokenV22InstanceExtended;

const getFiatToken = (
signatureBytesType: SignatureBytesType
): (() => AnyFiatTokenV2Instance) => {
return () => {
initializeOverloadedMethods(fiatToken, signatureBytesType);
return fiatToken;
};
};

beforeEach(async () => {
const [, , lostAndFound] = accounts;

fiatToken = await FiatTokenV2_2.new();
await initializeToVersion(fiatToken, "2.2", fiatTokenOwner, lostAndFound);
});

behavesLikeFiatTokenV2(accounts, () => fiatToken, fiatTokenOwner);
behavesLikeFiatTokenV2(
accounts,
getFiatToken(SignatureBytesType.Unpacked),
fiatTokenOwner
);

behavesLikeFiatTokenV22(
accounts,
getFiatToken(SignatureBytesType.Packed),
fiatTokenOwner
);
});

export function behavesLikeFiatTokenV22(
accounts: Truffle.Accounts,
getFiatToken: () => AnyFiatTokenV2Instance,
fiatTokenOwner: string
): void {
let domainSeparator: string;

beforeEach(async () => {
domainSeparator = makeDomainSeparator(
"USD Coin",
"2",
1, // hardcoded to 1 because of ganache bug: https://github.com/trufflesuite/ganache/issues/1643
getFiatToken().address
);
});

const v22TestParams = {
getFiatToken,
getDomainSeparator: () => domainSeparator,
getERC1271Wallet,
fiatTokenOwner,
accounts,
};

// Test gas abstraction funtionalities with both EOA and AA wallets
hasGasAbstraction({
...v22TestParams,
signerWalletType: WalletType.EOA,
signatureBytesType: SignatureBytesType.Packed,
});
hasGasAbstraction({
...v22TestParams,
signerWalletType: WalletType.AA,
signatureBytesType: SignatureBytesType.Packed,
});

describe("initializeV2_2", () => {
it("disallows calling initializeV2_2 twice", async () => {
await expectRevert(fiatToken.initializeV2_2());
await expectRevert(
(getFiatToken() as FiatTokenV22InstanceExtended).initializeV2_2()
);
});
});
});
}

/**
* With v2.2 we introduce overloaded functions for `permit`,
* `transferWithAuthorization`, `receiveWithAuthorization`,
* and `cancelAuthorization`.
*
* Since function overloading isn't supported by Javascript,
* the typechain library generates type interfaces for overloaded functions differently.
* For instance, we can no longer access the `permit` function with
* `fiattoken.permit`. Instead, we need to need to use the full function signature e.g.
* `fiattoken.methods["permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"]` OR
* `fiattoken.methods["permit(address,address,uint256,uint256,bytes)"]` (v22 interface).
*
* To preserve type-coherence and reuse test suites written for v2 & v2.1 contracts,
* here we re-assign the overloaded method definition to the method name shorthand.
*/
export function initializeOverloadedMethods(
fiatToken: FiatTokenV22InstanceExtended,
signatureBytesType: SignatureBytesType
): void {
if (signatureBytesType == SignatureBytesType.Unpacked) {
fiatToken.permit = fiatToken.methods[permitSignature];
fiatToken.transferWithAuthorization =
fiatToken.methods[transferWithAuthorizationSignature];
fiatToken.receiveWithAuthorization =
fiatToken.methods[receiveWithAuthorizationSignature];
fiatToken.cancelAuthorization =
fiatToken.methods[cancelAuthorizationSignature];
} else {
fiatToken.permit = fiatToken.methods[permitSignatureV22];
fiatToken.transferWithAuthorization =
fiatToken.methods[transferWithAuthorizationSignatureV22];
fiatToken.receiveWithAuthorization =
fiatToken.methods[receiveWithAuthorizationSignatureV22];
fiatToken.cancelAuthorization =
fiatToken.methods[cancelAuthorizationSignatureV22];
}
}
17 changes: 1 addition & 16 deletions test/v2/GasAbstraction/GasAbstraction.behavior.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,15 @@
import { FiatTokenV2Instance } from "../../../@types/generated";
import { TestParams } from "./helpers";
import { testTransferWithAuthorization } from "./testTransferWithAuthorization";
import { testCancelAuthorization } from "./testCancelAuthorization";
import { testPermit } from "./testPermit";
import { testTransferWithMultipleAuthorizations } from "./testTransferWithMultipleAuthorizations";
import { testReceiveWithAuthorization } from "./testReceiveWithAuthorization";

export function hasGasAbstraction(
getFiatToken: () => FiatTokenV2Instance,
getDomainSeparator: () => string,
fiatTokenOwner: string,
accounts: Truffle.Accounts
): void {
export function hasGasAbstraction(testParams: TestParams): void {
describe("GasAbstraction", () => {
const testParams: TestParams = {
getFiatToken,
getDomainSeparator,
fiatTokenOwner,
accounts,
};

describe("EIP-3009", () => {
testTransferWithAuthorization(testParams);
testReceiveWithAuthorization(testParams);
testCancelAuthorization(testParams);
testTransferWithMultipleAuthorizations(testParams);
});

describe("EIP-2612", () => {
Expand Down
Loading

0 comments on commit ad75174

Please sign in to comment.