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

Token Withdrawal Module for token withdrawals on behalf of IPAccount #131

Merged
merged 5 commits into from
Feb 23, 2024
Merged
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
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(
Comment on lines -1529 to +1530
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to keep the MockTokenManagementModule, the TokenWithdrawalModule` will be moved to the periphery repo. And we should cover such tests in core repo.

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
Loading