Skip to content
This repository has been archived by the owner on Apr 30, 2024. It is now read-only.

Commit

Permalink
Token Management Module for token transfers on behalf of IPAccount (#131
Browse files Browse the repository at this point in the history
)

* feat: Token Management Module (Withdrawal) for token transfers on behalf of IPAccounts
* feat: ERC Withdrawal module with restriction on receiver as IPA owner
  • Loading branch information
jdubpark authored Feb 23, 2024
1 parent d2eb0f8 commit e44c0b9
Show file tree
Hide file tree
Showing 6 changed files with 524 additions and 120 deletions.
43 changes: 43 additions & 0 deletions contracts/interfaces/modules/external/ITokenWithdrawalModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import { IModule } from "../base/IModule.sol";

/// @title Token Management Module
/// @notice Module for transferring ERC20, ERC721, and ERC1155 tokens for IP Accounts.
/// @dev SECURITY RISK: An IPAccount can delegate to a frontend contract (not a registered module) to transfer tokens
/// on behalf of the IPAccount via the Token Management Module. This frontend contract can transfer any tokens that are
/// approved by the IPAccount for the Token Management Module. In other words, there's no mechanism for this module to
/// granularly control which token a caller (approved contract in this case) can transfer.
interface ITokenWithdrawalModule is IModule {
/// @notice Withdraws ERC20 token from the IP account to the IP account owner.
/// @dev When calling this function, the caller must have the permission to call `transfer` via the IP account.
/// @dev Does not support transfer of multiple tokens at once.
/// @param ipAccount The IP account to transfer the ERC20 token from
/// @param tokenContract The address of the ERC20 token contract
/// @param amount The amount of token to transfer
function withdrawERC20(address payable ipAccount, address tokenContract, uint256 amount) external;

/// @notice Withdraws ERC721 token from the IP account to the IP account owner.
/// @dev When calling this function, the caller must have the permission to call `transferFrom` via the IP account.
/// @dev Does not support batch transfers.
/// @param ipAccount The IP account to transfer the ERC721 token from
/// @param tokenContract The address of the ERC721 token contract
/// @param tokenId The ID of the token to transfer
function withdrawERC721(address payable ipAccount, address tokenContract, uint256 tokenId) external;

/// @notice Withdraws ERC1155 token from the IP account to the IP account owner.
/// @dev When calling this function, the caller must have the permission to call `safeTransferFrom` via the IP
/// account.
/// @dev Does not support batch transfers.
/// @param ipAccount The IP account to transfer the ERC1155 token from
/// @param tokenContract The address of the ERC1155 token contract
/// @param tokenId The ID of the token to transfer
/// @param amount The amount of token to transfer
function withdrawERC1155(
address payable ipAccount,
address tokenContract,
uint256 tokenId,
uint256 amount
) external;
}
2 changes: 2 additions & 0 deletions contracts/lib/modules/Module.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ string constant LICENSING_MODULE_KEY = "LICENSING_MODULE";
string constant DISPUTE_MODULE_KEY = "DISPUTE_MODULE";

string constant ROYALTY_MODULE_KEY = "ROYALTY_MODULE";

string constant TOKEN_WITHDRAWAL_MODULE_KEY = "TOKEN_MANAGEMENT_MODULE";
99 changes: 99 additions & 0 deletions contracts/modules/external/TokenWithdrawalModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";

import { IIPAccount } from "../../interfaces/IIPAccount.sol";
import { IIPAccountRegistry } from "../../interfaces/registries/IIPAccountRegistry.sol";
import { ITokenWithdrawalModule } from "../../interfaces/modules/external/ITokenWithdrawalModule.sol";
import { IPAccountChecker } from "../../lib/registries/IPAccountChecker.sol";
import { TOKEN_WITHDRAWAL_MODULE_KEY } from "../../lib/modules/Module.sol";
import { BaseModule } from "../BaseModule.sol";
import { AccessControlled } from "../../access/AccessControlled.sol";

/// @title Token Management Module
/// @notice Module for transferring ERC20, ERC721, and ERC1155 tokens for IP Accounts.
/// @dev SECURITY RISK: An IPAccount can delegate to a frontend contract (not a registered module) to transfer tokens
/// on behalf of the IPAccount via the Token Management Module. This frontend contract can transfer any tokens that are
/// approved by the IPAccount for the Token Management Module. In other words, there's no mechanism for this module to
/// granularly control which token a caller (approved contract in this case) can transfer.
contract TokenWithdrawalModule is AccessControlled, BaseModule, ITokenWithdrawalModule {
using ERC165Checker for address;
using IPAccountChecker for IIPAccountRegistry;

string public constant override name = TOKEN_WITHDRAWAL_MODULE_KEY;

constructor(
address accessController,
address ipAccountRegistry
) AccessControlled(accessController, ipAccountRegistry) {}

/// @notice Withdraws ERC20 token from the IP account to the IP account owner.
/// @dev When calling this function, the caller must have the permission to call `transfer` via the IP account.
/// @dev Does not support transfer of multiple tokens at once.
/// @param ipAccount The IP account to transfer the ERC20 token from
/// @param tokenContract The address of the ERC20 token contract
/// @param amount The amount of token to transfer
function withdrawERC20(
address payable ipAccount,
address tokenContract,
uint256 amount
) external verifyPermission(ipAccount) {
IIPAccount(ipAccount).execute(
tokenContract,
0,
abi.encodeWithSignature("transfer(address,uint256)", IIPAccount(ipAccount).owner(), amount)
);
}

/// @notice Withdraws ERC721 token from the IP account to the IP account owner.
/// @dev When calling this function, the caller must have the permission to call `transferFrom` via the IP account.
/// @dev Does not support batch transfers.
/// @param ipAccount The IP account to transfer the ERC721 token from
/// @param tokenContract The address of the ERC721 token contract
/// @param tokenId The ID of the token to transfer
function withdrawERC721(
address payable ipAccount,
address tokenContract,
uint256 tokenId
) external verifyPermission(ipAccount) {
IIPAccount(ipAccount).execute(
tokenContract,
0,
abi.encodeWithSignature(
"transferFrom(address,address,uint256)",
ipAccount,
IIPAccount(ipAccount).owner(),
tokenId
)
);
}

/// @notice Withdraws ERC1155 token from the IP account to the IP account owner.
/// @dev When calling this function, the caller must have the permission to call `safeTransferFrom` via the IP
/// account.
/// @dev Does not support batch transfers.
/// @param ipAccount The IP account to transfer the ERC1155 token from
/// @param tokenContract The address of the ERC1155 token contract
/// @param tokenId The ID of the token to transfer
/// @param amount The amount of token to transfer
function withdrawERC1155(
address payable ipAccount,
address tokenContract,
uint256 tokenId,
uint256 amount
) external verifyPermission(ipAccount) {
IIPAccount(ipAccount).execute(
tokenContract,
0,
abi.encodeWithSignature(
"safeTransferFrom(address,address,uint256,uint256,bytes)",
ipAccount,
IIPAccount(ipAccount).owner(),
tokenId,
amount,
""
)
);
}
}
59 changes: 23 additions & 36 deletions test/foundry/AccessController.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ pragma solidity ^0.8.23;
import { IIPAccount } from "../../contracts/interfaces/IIPAccount.sol";
import { AccessPermission } from "../../contracts/lib/AccessPermission.sol";
import { Errors } from "../../contracts/lib/Errors.sol";
import { TOKEN_WITHDRAWAL_MODULE_KEY } from "../../contracts/lib/modules/Module.sol";
import { TokenWithdrawalModule } from "../../contracts/modules/external/TokenWithdrawalModule.sol";

import { MockModule } from "./mocks/module/MockModule.sol";
import { MockOrchestratorModule } from "./mocks/module/MockOrchestratorModule.sol";
import { MockTokenManagementModule } from "./mocks/module/MockTokenManagementModule.sol";
import { MockERC1155 } from "./mocks/token/MockERC1155.sol";
import { MockERC20 } from "./mocks/token/MockERC20.sol";
import { BaseTest } from "./utils/BaseTest.t.sol";
Expand Down Expand Up @@ -1526,20 +1527,19 @@ contract AccessControllerTest is BaseTest {

address anotherAccount = vm.addr(3);

MockTokenManagementModule tokenManagementModule = new MockTokenManagementModule(
TokenWithdrawalModule tokenWithdrawalModule = new TokenWithdrawalModule(
address(accessController),
address(ipAccountRegistry),
address(moduleRegistry)
address(ipAccountRegistry)
);
moduleRegistry.registerModule("MockTokenManagementModule", address(tokenManagementModule));
moduleRegistry.registerModule(TOKEN_WITHDRAWAL_MODULE_KEY, address(tokenWithdrawalModule));
vm.prank(owner);
ipAccount.execute(
address(accessController),
0,
abi.encodeWithSignature(
"setPermission(address,address,address,bytes4,uint8)",
address(ipAccount),
address(tokenManagementModule),
address(tokenWithdrawalModule),
address(mockNFT),
mockNFT.transferFrom.selector,
AccessPermission.ALLOW
Expand All @@ -1548,20 +1548,15 @@ contract AccessControllerTest is BaseTest {
assertEq(
accessController.getPermission(
address(ipAccount),
address(tokenManagementModule),
address(tokenWithdrawalModule),
address(mockNFT),
mockNFT.transferFrom.selector
),
AccessPermission.ALLOW
);
vm.prank(owner);
tokenManagementModule.transferERC721Token(
payable(address(ipAccount)),
anotherAccount,
address(mockNFT),
tokenId
);
assertEq(mockNFT.ownerOf(tokenId), anotherAccount);
tokenWithdrawalModule.withdrawERC721(payable(address(ipAccount)), address(mockNFT), tokenId);
assertEq(mockNFT.ownerOf(tokenId), owner);
}

// ipAccount transfer ERC1155 to another account
Expand All @@ -1572,20 +1567,19 @@ contract AccessControllerTest is BaseTest {

address anotherAccount = vm.addr(3);

MockTokenManagementModule tokenManagementModule = new MockTokenManagementModule(
TokenWithdrawalModule tokenWithdrawalModule = new TokenWithdrawalModule(
address(accessController),
address(ipAccountRegistry),
address(moduleRegistry)
address(ipAccountRegistry)
);
moduleRegistry.registerModule("MockTokenManagementModule", address(tokenManagementModule));
moduleRegistry.registerModule(TOKEN_WITHDRAWAL_MODULE_KEY, address(tokenWithdrawalModule));
vm.prank(owner);
ipAccount.execute(
address(accessController),
0,
abi.encodeWithSignature(
"setPermission(address,address,address,bytes4,uint8)",
address(ipAccount),
address(tokenManagementModule),
address(tokenWithdrawalModule),
address(mock1155),
mock1155.safeTransferFrom.selector,
AccessPermission.ALLOW
Expand All @@ -1594,21 +1588,15 @@ contract AccessControllerTest is BaseTest {
assertEq(
accessController.getPermission(
address(ipAccount),
address(tokenManagementModule),
address(tokenWithdrawalModule),
address(mock1155),
mock1155.safeTransferFrom.selector
),
AccessPermission.ALLOW
);
vm.prank(owner);
tokenManagementModule.transferERC1155Token(
payable(address(ipAccount)),
anotherAccount,
address(mock1155),
tokenId,
1e18
);
assertEq(mock1155.balanceOf(anotherAccount, tokenId), 1e18);
tokenWithdrawalModule.withdrawERC1155(payable(address(ipAccount)), address(mock1155), tokenId, 1e18);
assertEq(mock1155.balanceOf(owner, tokenId), 1e18);
}
// ipAccount transfer ERC20 to another account
function test_AccessController_ERC20Transfer() public {
Expand All @@ -1617,20 +1605,19 @@ contract AccessControllerTest is BaseTest {

address anotherAccount = vm.addr(3);

MockTokenManagementModule tokenManagementModule = new MockTokenManagementModule(
TokenWithdrawalModule tokenWithdrawalModule = new TokenWithdrawalModule(
address(accessController),
address(ipAccountRegistry),
address(moduleRegistry)
address(ipAccountRegistry)
);
moduleRegistry.registerModule("MockTokenManagementModule", address(tokenManagementModule));
moduleRegistry.registerModule(TOKEN_WITHDRAWAL_MODULE_KEY, address(tokenWithdrawalModule));
vm.prank(owner);
ipAccount.execute(
address(accessController),
0,
abi.encodeWithSignature(
"setPermission(address,address,address,bytes4,uint8)",
address(ipAccount),
address(tokenManagementModule),
address(tokenWithdrawalModule),
address(mock20),
mock20.transfer.selector,
AccessPermission.ALLOW
Expand All @@ -1639,14 +1626,14 @@ contract AccessControllerTest is BaseTest {
assertEq(
accessController.getPermission(
address(ipAccount),
address(tokenManagementModule),
address(tokenWithdrawalModule),
address(mock20),
mock20.transfer.selector
),
AccessPermission.ALLOW
);
vm.prank(owner);
tokenManagementModule.transferERC20Token(payable(address(ipAccount)), anotherAccount, address(mock20), 1e18);
assertEq(mock20.balanceOf(anotherAccount), 1e18);
tokenWithdrawalModule.withdrawERC20(payable(address(ipAccount)), address(mock20), 1e18);
assertEq(mock20.balanceOf(owner), 1e18);
}
}
84 changes: 0 additions & 84 deletions test/foundry/mocks/module/MockTokenManagementModule.sol

This file was deleted.

Loading

0 comments on commit e44c0b9

Please sign in to comment.