Skip to content

Commit

Permalink
Merge pull request #19 from MetaMask/ERC721TransferEnforcer
Browse files Browse the repository at this point in the history
Add ERC721TransferEnforcer
  • Loading branch information
dylandesrosier authored Sep 30, 2024
2 parents 78d603b + c4de27a commit d7d5bee
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 12 deletions.
4 changes: 1 addition & 3 deletions src/enforcers/BlockNumberEnforcer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ contract BlockNumberEnforcer is CaveatEnforcer {
* @return blockAfterThreshold_ The earliest block number before which the delegation can be used.
* @return blockBeforeThreshold_ The latest block number after which the delegation can be used.
*/
function getTermsInfo(
bytes calldata _terms
)
function getTermsInfo(bytes calldata _terms)
public
pure
returns (uint128 blockAfterThreshold_, uint128 blockBeforeThreshold_)
Expand Down
4 changes: 1 addition & 3 deletions src/enforcers/DeployedEnforcer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ contract DeployedEnforcer is CaveatEnforcer {
* @return salt_ The salt to use for create2.
* @return bytecode_ The bytecode of the contract to deploy.
*/
function getTermsInfo(
bytes calldata _terms
)
function getTermsInfo(bytes calldata _terms)
public
pure
returns (address expectedAddress_, bytes32 salt_, bytes memory bytecode_)
Expand Down
65 changes: 65 additions & 0 deletions src/enforcers/ERC721TransferEnforcer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import { CaveatEnforcer } from "./CaveatEnforcer.sol";
import { ModeCode } from "../utils/Types.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";

/**
* @title ERC721TransferEnforcer
* @notice This enforcer restricts the action of a UserOp to the transfer of a specific ERC721 token.
*/
contract ERC721TransferEnforcer is CaveatEnforcer {
/**
* @notice Enforces that the contract and tokenId are permitted for transfer
* @param _terms abi encoded address of the contract and uint256 of the tokenId
* @param _mode the execution mode of the transaction
* @param _executionCallData the call data of the transferFrom call
*/
function beforeHook(
bytes calldata _terms,
bytes calldata,
ModeCode _mode,
bytes calldata _executionCallData,
bytes32,
address,
address
)
public
virtual
override
onlySingleExecutionMode(_mode)
{
(address permittedContract_, uint256 permittedTokenId_) = getTermsInfo(_terms);
(address target_,, bytes calldata callData_) = ExecutionLib.decodeSingle(_executionCallData);
bytes4 selector_ = bytes4(callData_[0:4]);

// Decode the remaining callData into NFT transfer parameters
// The calldata should be at least 100 bytes (4 bytes for the selector + 96 bytes for the parameters)
if (callData_.length < 100) {
revert("ERC721TransferEnforcer:invalid-calldata-length");
}

// Decode the remaining callData into NFT transfer parameters
(address from_, address to_, uint256 transferTokenId_) = abi.decode(callData_[4:], (address, address, uint256));

if (from_ == address(0) || to_ == address(0)) {
revert("ERC721TransferEnforcer:invalid-address");
}

if (target_ != permittedContract_) {
revert("ERC721TransferEnforcer:unauthorized-contract-target");
} else if (selector_ != IERC721.transferFrom.selector) {
revert("ERC721TransferEnforcer:unauthorized-selector");
} else if (transferTokenId_ != permittedTokenId_) {
revert("ERC721TransferEnforcer:unauthorized-token-id");
}
}

function getTermsInfo(bytes calldata _terms) public pure returns (address permittedContract_, uint256 permittedTokenId_) {
if (_terms.length != 52) revert("ERC721TransferEnforcer:invalid-terms-length");
permittedContract_ = address(bytes20(_terms[:20]));
permittedTokenId_ = uint256(bytes32(_terms[20:]));
}
}
4 changes: 1 addition & 3 deletions src/enforcers/TimestampEnforcer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ contract TimestampEnforcer is CaveatEnforcer {
* @return timestampAfterThreshold_ The earliest timestamp before which the delegation can be used.
* @return timestampBeforeThreshold_ The latest timestamp after which the delegation can be used.
*/
function getTermsInfo(
bytes calldata _terms
)
function getTermsInfo(bytes calldata _terms)
public
pure
returns (uint128 timestampAfterThreshold_, uint128 timestampBeforeThreshold_)
Expand Down
4 changes: 1 addition & 3 deletions src/libraries/P256VerifierLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ library P256VerifierLib {
* @param _signature The signature to be decoded
* @return decodedSig the decoded signature
*/
function _decodeWebAuthnP256Signature(
bytes memory _signature
)
function _decodeWebAuthnP256Signature(bytes memory _signature)
internal
pure
returns (DecodedWebAuthnSignature memory decodedSig)
Expand Down
153 changes: 153 additions & 0 deletions test/enforcers/ERC721TransferEnforcer.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity 0.8.23;

import "forge-std/Test.sol";
import { ModeLib } from "@erc7579/lib/ModeLib.sol";
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";

import { Execution, Caveat, Delegation, ModeCode } from "../../src/utils/Types.sol";
import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol";
import { ERC721TransferEnforcer } from "../../src/enforcers/ERC721TransferEnforcer.sol";
import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol";
import { EncoderLib } from "../../src/libraries/EncoderLib.sol";
import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract ERC721TransferEnforcerTest is CaveatEnforcerBaseTest {
using ModeLib for ModeCode;

////////////////////// State //////////////////////

ERC721TransferEnforcer public erc721TransferEnforcer;
ModeCode public mode = ModeLib.encodeSimpleSingle();
IERC721 public mockNFT;
address public constant NFT_CONTRACT = address(0x1234567890123456789012345678901234567890);
uint256 public constant TOKEN_ID = 42;

////////////////////// Set up //////////////////////

function setUp() public override {
super.setUp();
erc721TransferEnforcer = new ERC721TransferEnforcer();
vm.label(address(erc721TransferEnforcer), "ERC721 Transfer Enforcer");
mockNFT = IERC721(NFT_CONTRACT);
}

////////////////////// Valid cases //////////////////////
function test_validTransfer() public {
Execution memory execution_ = Execution({
target: NFT_CONTRACT,
value: 0,
callData: abi.encodeWithSelector(IERC721.transferFrom.selector, address(this), address(0xBEEF), TOKEN_ID)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

vm.prank(address(delegationManager));
erc721TransferEnforcer.beforeHook(
abi.encodePacked(NFT_CONTRACT, TOKEN_ID), hex"", mode, executionCallData_, keccak256(""), address(0), address(0)
);
}

////////////////////// Invalid cases //////////////////////

function test_invalidTermsLength() public {
vm.expectRevert("ERC721TransferEnforcer:invalid-terms-length");
erc721TransferEnforcer.getTermsInfo(abi.encodePacked(NFT_CONTRACT));
}

function test_unauthorizedTransfer_wrongContract() public {
Execution memory execution_ = Execution({
target: address(0xDEAD),
value: 0,
callData: abi.encodeWithSelector(IERC721.transferFrom.selector, address(this), address(0xBEEF), TOKEN_ID)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

vm.prank(address(delegationManager));
vm.expectRevert("ERC721TransferEnforcer:unauthorized-contract-target");
erc721TransferEnforcer.beforeHook(
abi.encodePacked(NFT_CONTRACT, TOKEN_ID), hex"", mode, executionCallData_, keccak256(""), address(0), address(0)
);
}

function test_unauthorizedSelector_wrongMethod() public {
Execution memory execution_ = Execution({
target: NFT_CONTRACT,
value: 0,
callData: abi.encodeWithSignature("safeTransferFrom(address,address,uint256)", address(this), address(0xBEEF), TOKEN_ID)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

vm.prank(address(delegationManager));
vm.expectRevert("ERC721TransferEnforcer:unauthorized-selector");
erc721TransferEnforcer.beforeHook(
abi.encodePacked(NFT_CONTRACT, TOKEN_ID), hex"", mode, executionCallData_, keccak256(""), address(0), address(0)
);
}

function test_unauthorizedTransfer_wrongTokenId() public {
Execution memory execution_ = Execution({
target: NFT_CONTRACT,
value: 0,
callData: abi.encodeWithSelector(IERC721.transferFrom.selector, address(this), address(0xBEEF), TOKEN_ID + 1)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

vm.prank(address(delegationManager));
vm.expectRevert("ERC721TransferEnforcer:unauthorized-token-id");
erc721TransferEnforcer.beforeHook(
abi.encodePacked(NFT_CONTRACT, TOKEN_ID), hex"", mode, executionCallData_, keccak256(""), address(0), address(0)
);
}

function test_unauthorizedTransfer_wrongSelector() public {
Execution memory execution_ = Execution({
target: NFT_CONTRACT,
value: 0,
callData: abi.encodeWithSelector(IERC721.approve.selector, address(0xBEEF), TOKEN_ID)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

vm.prank(address(delegationManager));
vm.expectRevert("ERC721TransferEnforcer:invalid-calldata-length");
erc721TransferEnforcer.beforeHook(
abi.encodePacked(NFT_CONTRACT, TOKEN_ID), hex"", mode, executionCallData_, keccak256(""), address(0), address(0)
);
}

////////////////////// Integration //////////////////////

function test_validTransferIntegration() public {
Execution memory execution_ = Execution({
target: NFT_CONTRACT,
value: 0,
callData: abi.encodeWithSelector(
IERC721.transferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), TOKEN_ID
)
});

Caveat[] memory caveats_ = new Caveat[](1);
caveats_[0] =
Caveat({ args: hex"", enforcer: address(erc721TransferEnforcer), terms: abi.encodePacked(NFT_CONTRACT, TOKEN_ID) });
Delegation memory delegation_ = Delegation({
delegate: address(users.bob.deleGator),
delegator: address(users.alice.deleGator),
authority: ROOT_AUTHORITY,
caveats: caveats_,
salt: 0,
signature: hex""
});

delegation_ = signDelegation(users.alice, delegation_);

Delegation[] memory delegations_ = new Delegation[](1);
delegations_[0] = delegation_;

// Execute Bob's UserOp
invokeDelegation_UserOp(users.bob, delegations_, execution_);
}

function _getEnforcer() internal view override returns (ICaveatEnforcer) {
return ICaveatEnforcer(address(erc721TransferEnforcer));
}
}

0 comments on commit d7d5bee

Please sign in to comment.