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
9 changes: 9 additions & 0 deletions src/Permit3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,15 @@ contract Permit3 is IPermit3, MultiTokenPermit, NonceManager {
require(uint256(p.tokenKey) >> 160 == 0, InvalidTokenKeyForTransfer());

address token = address(uint160(uint256(p.tokenKey)));

// Gate signed transfers with the existing allowance lock mechanism (Cantina 3.1.2).
// The recipient (p.account) is the only counterparty committed in the signature, so an
// owner blocks a signed transfer to a given recipient via lockdown(token, recipient),
// which writes the same owner->tokenKey->recipient allowance slot read here.
Comment on lines +342 to +345
if (allowances[owner][p.tokenKey][p.account].expiration == LOCKED_ALLOWANCE) {
revert AllowanceLocked(owner, p.tokenKey, p.account);
}

_transferFrom(owner, p.account, p.amountDelta, token);
} else {
_processAllowanceOperation(owner, timestamp, p);
Expand Down
41 changes: 41 additions & 0 deletions test/Permit3.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,47 @@ contract Permit3Test is TestBase {
assertTrue(permit3.isNonceUsed(owner, SALT));
}

function test_permitTransferFrom_blockedByLockdownOnRecipient() public {
// Cantina 3.1.2: an owner blocks a signed transfer to a given recipient by
// calling lockdown(token, recipient), which locks the owner->token->recipient slot.
IPermit3.ChainPermits memory chainPermits = _createBasicTransferPermit();

deal(address(token), recipient, 0);

uint48 deadline = uint48(block.timestamp + 1 hours);
uint48 timestamp = uint48(block.timestamp);
bytes memory signature = _signPermit(chainPermits, deadline, timestamp, SALT);

// Owner locks down transfers to the recipient.
IPermit.TokenSpenderPair[] memory approvals = new IPermit.TokenSpenderPair[](1);
approvals[0] = IPermit.TokenSpenderPair({ token: address(token), spender: recipient });
vm.prank(owner);
permit3.lockdown(approvals);

// Submitting the signed transfer permit must now revert.
bytes32 tokenKey = bytes32(uint256(uint160(address(token))));
vm.expectRevert(abi.encodeWithSelector(IPermit.AllowanceLocked.selector, owner, tokenKey, recipient));
permit3.permit(owner, SALT, deadline, timestamp, chainPermits.permits, signature);

// Recipient balance unchanged.
assertEq(token.balanceOf(recipient), 0);
}

function test_permitTransferFrom_succeedsWithoutLockdown() public {
// Happy path with no lockdown still transfers successfully.
IPermit3.ChainPermits memory chainPermits = _createBasicTransferPermit();

deal(address(token), recipient, 0);

uint48 deadline = uint48(block.timestamp + 1 hours);
uint48 timestamp = uint48(block.timestamp);
bytes memory signature = _signPermit(chainPermits, deadline, timestamp, SALT);

permit3.permit(owner, SALT, deadline, timestamp, chainPermits.permits, signature);

assertEq(token.balanceOf(recipient), AMOUNT);
}
Comment on lines +64 to +77

function test_permitTransferFromExpired() public {
// Create the permit
IPermit3.ChainPermits memory chainPermits = _createBasicTransferPermit();
Expand Down
Loading