Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vyper approve2 #255

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4eea7e9
🚧 WIP
transmissions11 Jul 29, 2022
306ba1f
♻️ Inline safetransfer
transmissions11 Jul 29, 2022
b7ad213
👷‍♂️ Update vyper
transmissions11 Jul 29, 2022
44162d0
♻️ Reordering
transmissions11 Jul 29, 2022
8b136b9
♻️ Cleanup
transmissions11 Jul 30, 2022
2b9886e
⚡️ Optimize
transmissions11 Jul 30, 2022
7016aba
⚡️ Optimize
transmissions11 Jul 30, 2022
f50353d
🔥 Remove isOperator
transmissions11 Jul 30, 2022
7967ce4
♻️ Cleanup
transmissions11 Jul 30, 2022
e5fe831
♻️ Use ERC20 interface
transmissions11 Jul 30, 2022
fe70c4f
♻️ Use default_return_value
transmissions11 Jul 30, 2022
cea31b7
♻️ Use skip_contract_check
transmissions11 Jul 30, 2022
e7b777f
♻️ Cleanup
transmissions11 Jul 30, 2022
88fdd6b
♻️ Cleanup
transmissions11 Jul 30, 2022
fd0af78
♻️ Cleanup
transmissions11 Jul 30, 2022
7b5b413
👷‍♂️ Add vyper to CI
transmissions11 Jul 30, 2022
249b8f8
👷‍♂️ Build Vyper docs in CI
transmissions11 Jul 30, 2022
e0d37eb
📝 TODOs
transmissions11 Jul 30, 2022
6ad3b2f
♻️ Cleanup
transmissions11 Jul 30, 2022
4efdfe1
♻️ Cleanup
transmissions11 Jul 30, 2022
2ae0cf6
♻️ Cleanup
transmissions11 Jul 31, 2022
49fa8a5
📝 TODO
transmissions11 Jul 31, 2022
048fc00
✅ Lockdown draft
transmissions11 Aug 1, 2022
75a3a18
⚡️ Inline
transmissions11 Aug 1, 2022
0c700ab
Update Approve2.vy
transmissions11 Aug 1, 2022
5e18bcf
♻️ Compliant domain seperator
transmissions11 Aug 1, 2022
fd4d378
⚡️ Separate assert
transmissions11 Aug 1, 2022
717f9e0
♻️ Cleanup
transmissions11 Aug 1, 2022
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
24 changes: 13 additions & 11 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
Approve2Test:testOZSafePermit() (gas: 23395)
Approve2Test:testOZSafePermitPlusOZSafeTransferFrom() (gas: 129095)
Approve2Test:testInvalidateNonces() (gas: 27184)
Approve2Test:testLockdown() (gas: 432733)
Approve2Test:testOZSafePermit() (gas: 23418)
Approve2Test:testOZSafePermitPlusOZSafeTransferFrom() (gas: 129118)
Approve2Test:testOZSafeTransferFrom() (gas: 39310)
Approve2Test:testPermit2() (gas: 21763)
Approve2Test:testPermit2Full() (gas: 32804)
Approve2Test:testPermit2NonPermitToken() (gas: 22758)
Approve2Test:testPermit2PlusTransferFrom2() (gas: 126262)
Approve2Test:testPermit2PlusTransferFrom2WithNonPermit() (gas: 136552)
Approve2Test:testStandardPermit() (gas: 21427)
Approve2Test:testPermit2() (gas: 21797)
Approve2Test:testPermit2Full() (gas: 32243)
Approve2Test:testPermit2NonPermitToken() (gas: 22231)
Approve2Test:testPermit2PlusTransferFrom2() (gas: 126273)
Approve2Test:testPermit2PlusTransferFrom2WithNonPermit() (gas: 135604)
Approve2Test:testStandardPermit() (gas: 21405)
Approve2Test:testStandardTransferFrom() (gas: 38207)
Approve2Test:testTransferFrom2() (gas: 38086)
Approve2Test:testTransferFrom2Full() (gas: 47928)
Approve2Test:testTransferFrom2NonPermitToken() (gas: 47929)
Approve2Test:testTransferFrom2() (gas: 38064)
Approve2Test:testTransferFrom2Full() (gas: 47574)
Approve2Test:testTransferFrom2NonPermitToken() (gas: 47531)
11 changes: 11 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ jobs:
with:
version: nightly

- name: Set up python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8

- name: Install Vyper
run: pip install vyper

- name: Build Vyper docs
run: vyper -f devdoc src/*.vy

- name: Install dependencies
run: forge install

Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[profile.default]
ffi = true
solc = "0.8.15"
bytecode_hash = "none"
optimizer_runs = 1000000
Expand Down
147 changes: 34 additions & 113 deletions src/Approve2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ contract Approve2 {
// ensure no one accidentally invalidates all their nonces.
require(noncesToInvalidate <= type(uint16).max);

nonces[msg.sender] += noncesToInvalidate;
// Unchecked because counter overflow should
// be impossible on any reasonable timescale
// given the cap on noncesToInvalidate above.
unchecked {
nonces[msg.sender] += noncesToInvalidate;
}
}

/// @notice The EIP-712 "domain separator" the contract
/// will use when validating signatures for a given token.
/// @param token The token to get the domain separator for.
/// @dev For calls to permitAll, the address of
/// the Approve2 contract will be used the token.
function DOMAIN_SEPARATOR(address token) public view returns (bytes32) {
return
keccak256(
Expand All @@ -52,22 +55,10 @@ contract Approve2 {
ALLOWANCE STORAGE
//////////////////////////////////////////////////////////////*/

/// @notice Maps user addresses to "operator" addresses and whether they are
/// are approved to spend any amount of any token the user has approved.
mapping(address => mapping(address => bool)) public isOperator;

/// @notice Maps users to tokens to spender addresses and how much they
/// are approved to spend the amount of that token the user has approved.
mapping(address => mapping(ERC20 => mapping(address => uint256))) public allowance;

/// @notice Set whether an spender address is approved
/// to transfer any one of the sender's approved tokens.
/// @param operator The operator address to approve or unapprove.
/// @param approved Whether the operator is approved.
function setOperator(address operator, bool approved) external {
isOperator[msg.sender][operator] = approved;
}

/// @notice Approve a spender to transfer a specific
/// amount of a specific ERC20 token from the sender.
/// @param token The token to approve.
Expand Down Expand Up @@ -106,10 +97,12 @@ contract Approve2 {
bytes32 r,
bytes32 s
) external {
unchecked {
// Ensure the signature's deadline has not already passed.
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Ensure the signature's deadline has not already passed.
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");

// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
// Recover the signer address from the signature.
address recoveredAddress = ecrecover(
keccak256(
Expand Down Expand Up @@ -141,57 +134,6 @@ contract Approve2 {
}
}

/// @notice Permit a user to spend any amount of any of another
/// user's approved tokens via the owner's EIP-712 signature.
/// @param owner The user to permit spending from.
/// @param spender The user to permit spending to.
/// @param deadline The timestamp after which the signature is no longer valid.
/// @param v Must produce valid secp256k1 signature from the owner along with r and s.
/// @param r Must produce valid secp256k1 signature from the owner along with v and s.
/// @param s Must produce valid secp256k1 signature from the owner along with r and v.
/// @dev May fail if the owner's nonce was invalidated in-flight by invalidateNonce.
function permitAll(
address owner,
address spender,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
unchecked {
// Ensure the signature's deadline has not already passed.
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");

// Recover the signer address from the signature.
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(address(this)),
keccak256(
abi.encode(
keccak256("PermitAll(address owner,address spender,uint256 nonce,uint256 deadline)"),
owner,
spender,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);

// Ensure the signature is valid and the signer is the owner.
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");

// Set isOperator for the spender to true.
isOperator[owner][spender] = true;
}
}

/*//////////////////////////////////////////////////////////////
TRANSFER LOGIC
//////////////////////////////////////////////////////////////*/
Expand All @@ -209,62 +151,41 @@ contract Approve2 {
address to,
uint256 amount
) external {
unchecked {
uint256 allowed = allowance[from][token][msg.sender]; // Saves gas for limited approvals.
uint256 allowed = allowance[from][token][msg.sender]; // Saves gas for limited approvals.

// If the from address has set an unlimited approval, we'll go straight to the transfer.
if (allowed != type(uint256).max) {
if (allowed >= amount) {
// If msg.sender has enough approved to them, decrement their allowance.
allowance[from][token][msg.sender] = allowed - amount;
} else {
// Otherwise, check if msg.sender is an operator for the
// from address, otherwise we'll revert and block the transfer.
require(isOperator[from][msg.sender], "APPROVE_ALL_REQUIRED");
}
}
// If the from address has set an unlimited approval, we'll go straight to the transfer.
if (allowed != type(uint256).max) allowance[from][token][msg.sender] = allowed - amount;

// Transfer the tokens from the from address to the recipient.
token.safeTransferFrom(from, to, amount);
}
// Transfer the tokens from the from address to the recipient.
token.safeTransferFrom(from, to, amount);
}

/*//////////////////////////////////////////////////////////////
LOCKDOWN LOGIC
//////////////////////////////////////////////////////////////*/

// TODO: Bench if a struct for token-spender pairs is cheaper.
struct Approval {
ERC20 token;
address spender;
}

/// @notice Enables performing a "lockdown" of the sender's Approve2 identity
/// by batch revoking approvals, unapproving operators, and invalidating nonces.
/// @param tokens An array of tokens who's corresponding spenders should have their
/// approvals revoked. Each index should correspond to an index in the spenders array.
/// @param spenders An array of addresses to revoke approvals from.
/// Each index should correspond to an index in the tokens array.
/// @param operators An array of addresses to revoke operator approval from.
function lockdown(
ERC20[] calldata tokens,
address[] calldata spenders,
address[] calldata operators,
uint256 noncesToInvalidate
) external {
/// @notice Enables performing a "lockdown" of the sender's Approve2
/// identity by batch revoking approvals and invalidating nonces.
/// @param approvalsToRevoke An array of approvals to revoke.
/// @param noncesToInvalidate The number of nonces to invalidate.
function lockdown(Approval[] calldata approvalsToRevoke, uint256 noncesToInvalidate) external {
// Unchecked because counter overflow is impossible
// in any environment with reasonable gas limits.
unchecked {
// Will revert if trying to invalidate
// more than type(uint16).max nonces.
invalidateNonces(noncesToInvalidate);

// Each index should correspond to an index in the other array.
require(tokens.length == spenders.length, "LENGTH_MISMATCH");

// Revoke allowances for each pair of spenders and tokens.
for (uint256 i = 0; i < spenders.length; ++i) {
delete allowance[msg.sender][tokens[i]][spenders[i]];
}

// Revoke each of the sender's provided operator's powers.
for (uint256 i = 0; i < operators.length; ++i) {
delete isOperator[msg.sender][operators[i]];
for (uint256 i = 0; i < approvalsToRevoke.length; ++i) {
// TODO: Can this be optimized?
delete allowance[msg.sender][approvalsToRevoke[i].token][approvalsToRevoke[i].spender];
}
}

// Will revert if trying to invalidate
// more than type(uint16).max nonces.
invalidateNonces(noncesToInvalidate);
}
}
Loading