Skip to content

Commit

Permalink
feat(its-factory)!: support arbitrary destination minter for remote d…
Browse files Browse the repository at this point in the history
…eployment (#301)
  • Loading branch information
milapsheth authored Nov 12, 2024
1 parent 38698a2 commit 7e6916d
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/slither.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ jobs:
env:
NO_OVERRIDES: true
with:
node-version: 18
node-version: 20
slither-version: 0.10.1
125 changes: 120 additions & 5 deletions contracts/InterchainTokenFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,31 @@ import { IInterchainToken } from './interfaces/IInterchainToken.sol';
contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, Multicall, Upgradable {
using AddressBytes for address;

IInterchainTokenService public immutable interchainTokenService;
bytes32 public immutable chainNameHash;
/// @dev This slot contains the storage for this contract in an upgrade-compatible manner
/// keccak256('InterchainTokenFactory.Slot') - 1;
bytes32 internal constant INTERCHAIN_TOKEN_FACTORY_SLOT = 0xd4f5c43117c663161acfe6af3208a49856d85e586baf0f60749de2055e001465;

bytes32 private constant CONTRACT_ID = keccak256('interchain-token-factory');
bytes32 internal constant PREFIX_CANONICAL_TOKEN_SALT = keccak256('canonical-token-salt');
bytes32 internal constant PREFIX_INTERCHAIN_TOKEN_SALT = keccak256('interchain-token-salt');
bytes32 internal constant PREFIX_DEPLOY_APPROVAL = keccak256('deploy-approval');
address private constant TOKEN_FACTORY_DEPLOYER = address(0);

IInterchainTokenService public immutable interchainTokenService;
bytes32 public immutable chainNameHash;

struct DeployApproval {
address minter;
bytes32 tokenId;
string destinationChain;
}

/// @dev Storage for this contract
/// @param approvedDestinationMinters Mapping of approved destination minters
struct InterchainTokenFactoryStorage {
mapping(bytes32 => bytes32) approvedDestinationMinters;
}

/**
* @notice Constructs the InterchainTokenFactory contract.
* @param interchainTokenService_ The address of the interchain token service.
Expand Down Expand Up @@ -153,19 +170,93 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M
}
}

/**
* @notice Allow the minter to approve the deployer for a remote interchain token deployment that uses a custom destinationMinter address.
* This ensures that a token deployer can't choose the destinationMinter itself, and requires the approval of the minter to reduce trust assumptions on the deployer.
*/
function approveDeployRemoteInterchainToken(
address deployer,
bytes32 salt,
string calldata destinationChain,
bytes calldata destinationMinter
) external {
address minter = msg.sender;
bytes32 tokenId = interchainTokenId(deployer, salt);
IInterchainToken token = IInterchainToken(interchainTokenService.interchainTokenAddress(tokenId));
if (!token.isMinter(minter)) revert InvalidMinter(minter);

if (bytes(interchainTokenService.trustedAddress(destinationChain)).length == 0) revert InvalidChainName();

bytes32 approvalKey = _deployApprovalKey(DeployApproval({ minter: minter, tokenId: tokenId, destinationChain: destinationChain }));

_interchainTokenFactoryStorage().approvedDestinationMinters[approvalKey] = keccak256(destinationMinter);

emit DeployRemoteInterchainTokenApproval(minter, deployer, tokenId, destinationChain, destinationMinter);
}

/**
* @notice Allows the minter to revoke a deployer's approval for a remote interchain token deployment that uses a custom destinationMinter address.
*/
function revokeDeployRemoteInterchainToken(address deployer, bytes32 salt, string calldata destinationChain) external {
address minter = msg.sender;
bytes32 tokenId = interchainTokenId(deployer, salt);

bytes32 approvalKey = _deployApprovalKey(DeployApproval({ minter: minter, tokenId: tokenId, destinationChain: destinationChain }));

delete _interchainTokenFactoryStorage().approvedDestinationMinters[approvalKey];

emit RevokedDeployRemoteInterchainTokenApproval(minter, deployer, tokenId, destinationChain);
}

function _deployApprovalKey(DeployApproval memory approval) internal pure returns (bytes32 key) {
key = keccak256(abi.encode(PREFIX_DEPLOY_APPROVAL, approval));
}

function _useDeployApproval(DeployApproval memory approval, bytes memory destinationMinter) internal {
bytes32 approvalKey = _deployApprovalKey(approval);

InterchainTokenFactoryStorage storage slot = _interchainTokenFactoryStorage();

if (slot.approvedDestinationMinters[approvalKey] != keccak256(destinationMinter)) revert RemoteDeploymentNotApproved();

delete slot.approvedDestinationMinters[approvalKey];
}

/**
* @notice Deploys a remote interchain token on a specified destination chain.
* @param salt The unique salt for deploying the token.
* @param minter The address to use as the minter of the deployed token on the destination chain. If the destination chain is not EVM,
* then use the more generic `deployRemoteInterchainToken` function below that allows setting an arbitrary destination minter that was approved by the current minter.
* @param destinationChain The name of the destination chain.
* @param gasValue The amount of gas to send for the deployment.
* @return tokenId The tokenId corresponding to the deployed InterchainToken.
*/
function deployRemoteInterchainToken(
bytes32 salt,
address minter,
string memory destinationChain,
uint256 gasValue
) external payable returns (bytes32 tokenId) {
return deployRemoteInterchainTokenWithMinter(salt, minter, destinationChain, new bytes(0), gasValue);
}

/**
* @notice Deploys a remote interchain token on a specified destination chain.
* @param salt The unique salt for deploying the token.
* @param minter The address to receive the minter and operator role of the token, in addition to ITS. If the address is `address(0)`,
* no additional minter is set on the token. Reverts if the minter does not have mint permission for the token.
* @param destinationChain The name of the destination chain.
* @param destinationMinter The minter address to set on the deployed token on the destination chain. This can be arbitrary bytes
* since the encoding of the account is dependent on the destination chain. If this is empty, then the `minter` of the token on the current chain
* is used as the destination minter, which makes it convenient when deploying to other EVM chains.
* @param gasValue The amount of gas to send for the deployment.
* @return tokenId The tokenId corresponding to the deployed InterchainToken.
*/
function deployRemoteInterchainToken(
function deployRemoteInterchainTokenWithMinter(
bytes32 salt,
address minter,
string memory destinationChain,
bytes memory destinationMinter,
uint256 gasValue
) public payable returns (bytes32 tokenId) {
string memory tokenName;
Expand All @@ -184,9 +275,19 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M

if (minter != address(0)) {
if (!token.isMinter(minter)) revert NotMinter(minter);
// Sanity check to prevent accidental use of the current ITS address as the destination minter
if (minter == address(interchainTokenService)) revert InvalidMinter(minter);

minter_ = minter.toBytes();
if (destinationMinter.length > 0) {
DeployApproval memory approval = DeployApproval({ minter: minter, tokenId: tokenId, destinationChain: destinationChain });
_useDeployApproval(approval, destinationMinter);
minter_ = destinationMinter;
} else {
minter_ = minter.toBytes();
}
} else if (destinationMinter.length > 0) {
// If a destinationMinter is provided, then minter must not be address(0)
revert InvalidMinter(minter);
}

tokenId = _deployInterchainToken(salt, destinationChain, tokenName, tokenSymbol, tokenDecimals, minter_, gasValue);
Expand Down Expand Up @@ -214,7 +315,7 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M
) external payable returns (bytes32 tokenId) {
if (bytes(originalChainName).length != 0) revert NotSupported();

tokenId = deployRemoteInterchainToken(salt, minter, destinationChain, gasValue);
tokenId = deployRemoteInterchainTokenWithMinter(salt, minter, destinationChain, new bytes(0), gasValue);
}

/**
Expand Down Expand Up @@ -312,4 +413,18 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M

tokenId = deployRemoteCanonicalInterchainToken(originalTokenAddress, destinationChain, gasValue);
}

/********************\
|* Pure Key Getters *|
\********************/

/**
* @notice Gets the specific storage location for preventing upgrade collisions
* @return slot containing the storage struct
*/
function _interchainTokenFactoryStorage() private pure returns (InterchainTokenFactoryStorage storage slot) {
assembly {
slot.slot := INTERCHAIN_TOKEN_FACTORY_SLOT
}
}
}
56 changes: 55 additions & 1 deletion contracts/interfaces/IInterchainTokenFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ interface IInterchainTokenFactory is IUpgradable, IMulticall {
error NotOperator(address operator);
error NotServiceOwner(address sender);
error NotSupported();
error RemoteDeploymentNotApproved();

/// @notice Emitted when a minter approves a deployer for a remote interchain token deployment that uses a custom destinationMinter address.
event DeployRemoteInterchainTokenApproval(
address indexed minter,
address indexed deployer,
bytes32 indexed tokenId,
string destinationChain,
bytes destinationMinter
);

/// @notice Emitted when a minter revokes a deployer's approval for a remote interchain token deployment that uses a custom destinationMinter address.
event RevokedDeployRemoteInterchainTokenApproval(
address indexed minter,
address indexed deployer,
bytes32 indexed tokenId,
string destinationChain
);

/**
* @notice Returns the address of the interchain token service.
Expand Down Expand Up @@ -76,10 +94,27 @@ interface IInterchainTokenFactory is IUpgradable, IMulticall {
address minter
) external payable returns (bytes32 tokenId);

/**
* @notice Allows the minter to approve a deployer for a remote interchain token deployment that uses a custom destinationMinter address.
* This ensures that a token deployer can't choose the destinationMinter itself, and requires the approval of the minter to reduce trust assumptions on the deployer.
*/
function approveDeployRemoteInterchainToken(
address deployer,
bytes32 salt,
string calldata destinationChain,
bytes calldata destinationMinter
) external;

/**
* @notice Allows the minter to revoke a deployer's approval for a remote interchain token deployment that uses a custom destinationMinter address.
*/
function revokeDeployRemoteInterchainToken(address deployer, bytes32 salt, string calldata destinationChain) external;

/**
* @notice Deploys a remote interchain token on a specified destination chain.
* @param salt The unique salt for deploying the token.
* @param minter The address to distribute the token on the destination chain.
* @param minter The address to use as the minter of the deployed token on the destination chain. If the destination chain is not EVM,
* then use the more generic `deployRemoteInterchainToken` function below that allows setting an arbitrary destination minter that was approved by the current minter.
* @param destinationChain The name of the destination chain.
* @param gasValue The amount of gas to send for the deployment.
* @return tokenId The tokenId corresponding to the deployed InterchainToken.
Expand All @@ -91,6 +126,25 @@ interface IInterchainTokenFactory is IUpgradable, IMulticall {
uint256 gasValue
) external payable returns (bytes32 tokenId);

/**
* @notice Deploys a remote interchain token on a specified destination chain.
* @param salt The unique salt for deploying the token.
* @param minter The address to distribute the token on the destination chain.
* @param destinationChain The name of the destination chain.
* @param destinationMinter The minter address to set on the deployed token on the destination chain. This can be arbitrary bytes
* since the encoding of the account is dependent on the destination chain. If this is empty, then the `minter` of the token on the current chain
* is used as the destination minter, which makes it convenient when deploying to other EVM chains.
* @param gasValue The amount of gas to send for the deployment.
* @return tokenId The tokenId corresponding to the deployed InterchainToken.
*/
function deployRemoteInterchainTokenWithMinter(
bytes32 salt,
address minter,
string memory destinationChain,
bytes memory destinationMinter,
uint256 gasValue
) external payable returns (bytes32 tokenId);

/**
* @notice Deploys a remote interchain token on a specified destination chain.
* @dev originalChainName is only allowed to be '', i.e the current chain.
Expand Down
77 changes: 76 additions & 1 deletion test/InterchainTokenFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const {
getContractAt,
Wallet,
constants: { AddressZero },
utils: { defaultAbiCoder, keccak256, toUtf8Bytes },
utils: { defaultAbiCoder, keccak256, toUtf8Bytes, arrayify },
} = ethers;
const { deployAll, deployContract } = require('../scripts/deploy');
const { getRandomBytes32, expectRevert } = require('./utils');
Expand Down Expand Up @@ -423,6 +423,81 @@ describe('InterchainTokenFactory', () => {
.withArgs(service.address, destinationChain, service.address, keccak256(payload), gasValue, wallet.address)
.and.to.emit(gateway, 'ContractCall')
.withArgs(service.address, destinationChain, service.address, keccak256(payload), payload);

await expectRevert(
(gasOptions) =>
tokenFactory.deployRemoteInterchainTokenWithMinter(salt, wallet.address, destinationChain, wallet.address, gasValue, {
...gasOptions,
value: gasValue,
}),
tokenFactory,
'RemoteDeploymentNotApproved',
[],
);

await expectRevert(
(gasOptions) =>
tokenFactory.deployRemoteInterchainTokenWithMinter(salt, AddressZero, destinationChain, wallet.address, gasValue, {
...gasOptions,
value: gasValue,
}),
tokenFactory,
'InvalidMinter',
[AddressZero],
);

await expectRevert(
(gasOptions) =>
tokenFactory.approveDeployRemoteInterchainToken(wallet.address, salt, 'untrusted-chain', wallet.address, gasOptions),
tokenFactory,
'InvalidChainName',
[],
);

await expectRevert(
(gasOptions) =>
tokenFactory
.connect(otherWallet)
.approveDeployRemoteInterchainToken(wallet.address, salt, destinationChain, wallet.address, gasOptions),
tokenFactory,
'InvalidMinter',
[otherWallet.address],
);

await expect(tokenFactory.approveDeployRemoteInterchainToken(wallet.address, salt, destinationChain, wallet.address))
.to.emit(tokenFactory, 'DeployRemoteInterchainTokenApproval')
.withArgs(wallet.address, wallet.address, tokenId, destinationChain, arrayify(wallet.address));

await expect(tokenFactory.revokeDeployRemoteInterchainToken(wallet.address, salt, destinationChain))
.to.emit(tokenFactory, 'RevokedDeployRemoteInterchainTokenApproval')
.withArgs(wallet.address, wallet.address, tokenId, destinationChain);

await expectRevert(
(gasOptions) =>
tokenFactory.deployRemoteInterchainTokenWithMinter(salt, wallet.address, destinationChain, wallet.address, gasValue, {
...gasOptions,
value: gasValue,
}),
tokenFactory,
'RemoteDeploymentNotApproved',
[],
);

await expect(tokenFactory.approveDeployRemoteInterchainToken(wallet.address, salt, destinationChain, wallet.address))
.to.emit(tokenFactory, 'DeployRemoteInterchainTokenApproval')
.withArgs(wallet.address, wallet.address, tokenId, destinationChain, arrayify(wallet.address));

await expect(
tokenFactory.deployRemoteInterchainTokenWithMinter(salt, wallet.address, destinationChain, wallet.address, gasValue, {
value: gasValue,
}),
)
.to.emit(service, 'InterchainTokenDeploymentStarted')
.withArgs(tokenId, name, symbol, decimals, wallet.address.toLowerCase(), destinationChain)
.and.to.emit(gasService, 'NativeGasPaidForContractCall')
.withArgs(service.address, destinationChain, service.address, keccak256(payload), gasValue, wallet.address)
.and.to.emit(gateway, 'ContractCall')
.withArgs(service.address, destinationChain, service.address, keccak256(payload), payload);
});

it('Should initiate a remote interchain token deployment without the same minter', async () => {
Expand Down

0 comments on commit 7e6916d

Please sign in to comment.