Skip to content

Commit d658066

Browse files
committed
implement permit transfer asset from via permit2
1 parent bc393d5 commit d658066

5 files changed

Lines changed: 194 additions & 175 deletions

File tree

src/Permit2MultiToken.sol

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ pragma solidity ^0.8.0;
44
import { IERC20 } from "openzeppelin/interfaces/IERC20.sol";
55
import { IERC721 } from "openzeppelin/interfaces/IERC721.sol";
66
import { IERC1155 } from "openzeppelin/interfaces/IERC1155.sol";
7-
import { IERC20Permit } from "openzeppelin/token/ERC20/extensions/IERC20Permit.sol";
87
import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
98
import { ERC165Checker } from "openzeppelin/utils/introspection/ERC165Checker.sol";
109
import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol";
@@ -101,53 +100,53 @@ library Permit2MultiToken {
101100
|*----------------------------------------------------------*/
102101

103102
/**
104-
* @notice Wrapping function for `transferFrom` calls on various token interfaces.
103+
* @notice Wrapping function for `transferFrom` calls.
105104
* @dev If `source` is `address(this)`, function `transfer` is called instead of `transferFrom` for ERC20 category.
106105
* @param asset Struct defining all necessary context of a token.
107106
* @param permit2 Address of the Permit2 contract to be used for transferring ERC20 tokens.
108107
* @param source Account/address that provided the allowance.
109108
* @param dest Destination address.
110109
*/
111110
function transferAssetFrom(Asset memory asset, address permit2, address source, address dest) internal {
112-
_transferAssetFrom(asset, permit2, source, dest, false);
111+
if (asset.category != Category.ERC20) {
112+
revert("MultiToken: Unsupported category");
113+
}
114+
115+
if (source == address(this)) {
116+
IERC20(asset.assetAddress).safeTransfer(dest, asset.amount);
117+
} else {
118+
IPermit2Like(permit2).transferFrom(source, dest, asset.amount.toUint160(), asset.assetAddress);
119+
}
113120
}
114121

115122
/**
116-
* @notice Wrapping function for `safeTransferFrom` calls on various token interfaces.
123+
* @notice Wrapping function for `transferFrom` calls.
117124
* @dev If `source` is `address(this)`, function `transfer` is called instead of `transferFrom` for ERC20 category.
118125
* @param asset Struct defining all necessary context of a token.
119126
* @param permit2 Address of the Permit2 contract to be used for transferring ERC20 tokens.
120127
* @param source Account/address that provided the allowance.
121128
* @param dest Destination address.
129+
* @param permit PermitTransferFrom struct containing the signed permit data.
130+
* @param signature Signature to verify the permit.
122131
*/
123-
function safeTransferAssetFrom(Asset memory asset, address permit2, address source, address dest) internal {
124-
_transferAssetFrom(asset, permit2, source, dest, true);
125-
}
126-
127-
function _transferAssetFrom(Asset memory asset, address permit2, address source, address dest, bool isSafe) private {
128-
if (asset.category == Category.ERC20) {
129-
if (source == address(this))
130-
IERC20(asset.assetAddress).safeTransfer(dest, asset.amount);
131-
else
132-
IPermit2Like(permit2).transferFrom(source, dest, asset.amount.toUint160(), asset.assetAddress);
133-
134-
} else if (asset.category == Category.ERC721) {
135-
if (!isSafe)
136-
IERC721(asset.assetAddress).transferFrom(source, dest, asset.id);
137-
else
138-
IERC721(asset.assetAddress).safeTransferFrom(source, dest, asset.id, "");
139-
140-
} else if (asset.category == Category.ERC1155) {
141-
IERC1155(asset.assetAddress).safeTransferFrom(source, dest, asset.id, asset.amount == 0 ? 1 : asset.amount, "");
142-
143-
} else if (asset.category == Category.CryptoKitties) {
144-
if (source == address(this))
145-
ICryptoKitties(asset.assetAddress).transfer(dest, asset.id);
146-
else
147-
ICryptoKitties(asset.assetAddress).transferFrom(source, dest, asset.id);
132+
function permitTransferAssetFrom(
133+
Asset memory asset,
134+
address permit2,
135+
address source,
136+
address dest,
137+
IPermit2Like.PermitTransferFrom memory permit,
138+
bytes memory signature
139+
) internal {
140+
if (asset.category != Category.ERC20) {
141+
revert("MultiToken: Unsupported category");
142+
}
148143

144+
if (source == address(this)) {
145+
IERC20(asset.assetAddress).safeTransfer(dest, asset.amount);
149146
} else {
150-
revert("MultiToken: Unsupported category");
147+
IPermit2Like(permit2).permitTransferFrom(
148+
permit, IPermit2Like.SignatureTransferDetails(dest, asset.amount), source, signature
149+
);
151150
}
152151
}
153152

src/interfaces/IPermit2Like.sol

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,47 @@ pragma solidity ^0.8.0;
44

55
interface IPermit2Like {
66

7+
/** @notice The token and amount details for a transfer signed in the permit transfer signature */
8+
struct TokenPermissions {
9+
address token;
10+
uint256 amount;
11+
}
12+
13+
/** @notice The signed permit message for a single token transfer */
14+
struct PermitTransferFrom {
15+
TokenPermissions permitted;
16+
uint256 nonce;
17+
uint256 deadline;
18+
}
19+
20+
/**
21+
* @notice Specifies the recipient address and amount for batched transfers.
22+
* @dev Recipients and amounts correspond to the index of the signed token permissions array.
23+
* @dev Reverts if the requested amount is greater than the permitted signed amount.
24+
*/
25+
struct SignatureTransferDetails {
26+
address to;
27+
uint256 requestedAmount;
28+
}
29+
30+
/** @notice Returns the domain separator for the current chain. */
31+
function DOMAIN_SEPARATOR() external view returns (bytes32);
32+
33+
/**
34+
* @notice Transfers a token using a signed permit message
35+
* @dev Reverts if the requested amount is greater than the permitted signed amount
36+
* @param permit The permit data signed over by the owner
37+
* @param owner The owner of the tokens to transfer
38+
* @param transferDetails The spender's requested transfer details for the permitted token
39+
* @param signature The signature to verify
40+
*/
41+
function permitTransferFrom(
42+
PermitTransferFrom memory permit,
43+
SignatureTransferDetails calldata transferDetails,
44+
address owner,
45+
bytes calldata signature
46+
) external;
47+
748
/**
849
* @notice Transfer approved tokens from one address to another
950
* @dev Requires the from address to have approved at least the desired amount of tokens to msg.sender.

test/fork/Permit2MultiToken.fork.t.sol

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Test } from "forge-std/Test.sol";
55

66
import {
77
Permit2MultiToken, Asset, Category,
8-
98
IERC20, IPermit2Like
109
} from "multitoken/Permit2MultiToken.sol";
1110

@@ -18,13 +17,18 @@ abstract contract Permit2MultiTokenIntegrationTest is Test {
1817
address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
1918
address permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
2019

21-
address joey = makeAddr("joey");
22-
address chandler = makeAddr("chandler");
20+
address joey;
21+
uint256 joeyPK;
22+
address chandler;
23+
uint256 chandlerPK;
2324
uint160 amount = 1 ether;
2425

2526
function setUp() public virtual {
2627
vm.createSelectFork("mainnet");
2728

29+
(joey, joeyPK) = makeAddrAndKey("joey");
30+
(chandler, chandlerPK) = makeAddrAndKey("chandler");
31+
2832
vm.label(WETH, "WETH");
2933
vm.label(address(permit2), "Permit2");
3034
}
@@ -72,3 +76,63 @@ contract Permit2MultiToken_Transfer_IntegrationTest is Permit2MultiTokenIntegrat
7276
}
7377

7478
}
79+
80+
81+
/*----------------------------------------------------------*|
82+
|* # PERMIT TRANSFER *|
83+
|*----------------------------------------------------------*/
84+
85+
contract Permit2MultiToken_PermitTransfer_IntegrationTest is Permit2MultiTokenIntegrationTest {
86+
87+
function _sign(uint256 pk, bytes32 _hash) internal pure returns (bytes memory) {
88+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _hash);
89+
return abi.encodePacked(r, s, v);
90+
}
91+
92+
93+
function test_shouldUsePermit2ToTransferERC20() external {
94+
assertEq(IERC20(WETH).balanceOf(joey), 0);
95+
assertEq(IERC20(WETH).balanceOf(chandler), 0);
96+
97+
vm.prank(WETH); // Assuming WETH contract has at least `amount` tokens
98+
(bool success, ) = WETH.call(abi.encodeWithSignature("transfer(address,uint256)", joey, amount));
99+
require(success, "WETH initial test token transfer failed");
100+
101+
vm.prank(joey);
102+
IERC20(WETH).approve(permit2, type(uint256).max);
103+
104+
IPermit2Like.PermitTransferFrom memory permit = IPermit2Like.PermitTransferFrom({
105+
permitted: IPermit2Like.TokenPermissions(WETH, amount),
106+
nonce: 0,
107+
deadline: block.timestamp + 1 days
108+
});
109+
110+
bytes32 tokenPermissionsHash = keccak256(abi.encode(
111+
keccak256("TokenPermissions(address token,uint256 amount)"),
112+
permit.permitted
113+
));
114+
bytes32 permitHash = keccak256(abi.encode(
115+
keccak256("PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)"),
116+
tokenPermissionsHash, chandler, permit.nonce, permit.deadline
117+
));
118+
bytes32 domainSeparator = IPermit2Like(permit2).DOMAIN_SEPARATOR();
119+
bytes memory signature = _sign(
120+
joeyPK,
121+
keccak256(abi.encodePacked(hex"1901", domainSeparator, permitHash))
122+
);
123+
124+
vm.expectCall(
125+
permit2,
126+
abi.encodeWithSelector(IPermit2Like.permitTransferFrom.selector)
127+
);
128+
129+
vm.prank(chandler);
130+
WETH.ERC20(amount / 2).permitTransferAssetFrom(permit2, joey, chandler, permit, signature);
131+
132+
assertEq(IERC20(WETH).balanceOf(joey), amount / 2);
133+
assertEq(IERC20(WETH).balanceOf(chandler), amount / 2);
134+
135+
// Note: can use permit only once
136+
}
137+
138+
}
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.16;
33

4-
import { Permit2MultiToken, Asset } from "multitoken/Permit2MultiToken.sol";
4+
import { Permit2MultiToken, Asset, IPermit2Like } from "multitoken/Permit2MultiToken.sol";
55

66

77
contract Permit2MultiTokenHarness {
@@ -10,8 +10,15 @@ contract Permit2MultiTokenHarness {
1010
Permit2MultiToken.transferAssetFrom(asset, permit2, source, dest);
1111
}
1212

13-
function safeTransferAssetFrom(Asset memory asset, address permit2, address source, address dest) external {
14-
Permit2MultiToken.safeTransferAssetFrom(asset, permit2, source, dest);
13+
function permitTransferAssetFrom(
14+
Asset memory asset,
15+
address permit2,
16+
address source,
17+
address dest,
18+
IPermit2Like.PermitTransferFrom memory permit,
19+
bytes memory signature
20+
) external {
21+
Permit2MultiToken.permitTransferAssetFrom(asset, permit2, source, dest, permit, signature);
1522
}
1623

1724
}

0 commit comments

Comments
 (0)