Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: register custom tokens from the factory. #319

Open
wants to merge 3 commits into
base: feat-register-tokens-and-rename
Choose a base branch
from
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
48 changes: 48 additions & 0 deletions contracts/InterchainTokenFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M
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');
bytes32 internal constant PREFIX_CUSTOM_TOKEN_SALT = keccak256('custom-token-salt');
address private constant TOKEN_FACTORY_DEPLOYER = address(0);

IInterchainTokenService public immutable interchainTokenService;
Expand Down Expand Up @@ -490,6 +491,53 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M
tokenId = deployRemoteCanonicalInterchainToken(originalTokenAddress, destinationChain, gasValue);
}

function linkedTokenDeploySalt(address deployer, bytes32 salt) public view returns (bytes32 deploySalt) {
deploySalt = keccak256(abi.encode(PREFIX_CUSTOM_TOKEN_SALT, chainNameHash, deployer, salt));
}

function linkedTokenId(address deployer, bytes32 salt) external view returns (bytes32 tokenId) {
bytes32 deploySalt = linkedTokenDeploySalt(deployer, salt);
tokenId = _interchainTokenId(deploySalt);
}

function registerCustomToken(
bytes32 salt,
address tokenAddress,
TokenManagerType tokenManagerType,
address operator,
uint256 gasValue
) external payable returns (bytes32 tokenId) {
bytes32 deploySalt = linkedTokenDeploySalt(msg.sender, salt);
bytes memory operatorBytes = '';
string memory currentChain = '';
if (operator != address(0)) {
operatorBytes = operator.toBytes();
}

tokenId = interchainTokenService.linkToken(deploySalt, currentChain, tokenAddress.toBytes(), tokenManagerType, operatorBytes, 0);

interchainTokenService.registerTokenMetadata{ value: gasValue }(tokenAddress, gasValue);
}

function linkToken(
Copy link
Member

Choose a reason for hiding this comment

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

interface needs to be updated. docstring is also needed

bytes32 salt,
string calldata destinationChain,
bytes calldata destinationTokenAddress,
TokenManagerType tokenManagerType,
bytes calldata linkParams,
uint256 gasValue
) external payable returns (bytes32 tokenId) {
bytes32 deploySalt = linkedTokenDeploySalt(msg.sender, salt);
tokenId = interchainTokenService.linkToken(
deploySalt,
destinationChain,
destinationTokenAddress,
tokenManagerType,
linkParams,
gasValue
);
}

Copy link
Member

Choose a reason for hiding this comment

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

missing tests?

/********************\
|* Pure Key Getters *|
\********************/
Expand Down
256 changes: 255 additions & 1 deletion test/InterchainTokenFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ const {
utils: { defaultAbiCoder, keccak256, toUtf8Bytes, arrayify },
} = ethers;
const { deployAll, deployContract } = require('../scripts/deploy');
const { getRandomBytes32, expectRevert } = require('./utils');
const { getRandomBytes32, expectRevert, gasReporter } = require('./utils');
const {
MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN,
NATIVE_INTERCHAIN_TOKEN,
LOCK_UNLOCK,
MINTER_ROLE,
OPERATOR_ROLE,
FLOW_LIMITER_ROLE,
MINT_BURN,
MINT_BURN_FROM,
LOCK_UNLOCK_FEE_ON_TRANSFER,
} = require('./constants');
const { getBytecodeHash } = require('@axelar-network/axelar-chains-config');

const reportGas = gasReporter('Interchain Token Factory');

describe('InterchainTokenFactory', () => {
let wallet, otherWallet;
let service, gateway, gasService, tokenFactory;
Expand Down Expand Up @@ -617,5 +622,254 @@ describe('InterchainTokenFactory', () => {
.and.to.emit(gateway, 'ContractCall')
.withArgs(service.address, destinationChain, service.address, keccak256(payload), payload);
});

describe('Custom Token Manager Deployment', () => {
const tokenName = 'Token Name';
const tokenSymbol = 'TN';
const tokenDecimals = 13;
let token, salt, tokenId;
let tokenManagerProxy;
let factorySalt;

before(async () => {
salt = getRandomBytes32();
tokenId = await tokenFactory.linkedTokenId(wallet.address, salt);
token = await deployContract(wallet, 'TestInterchainTokenStandard', [
tokenName,
tokenSymbol,
tokenDecimals,
service.address,
tokenId,
]);
factorySalt = await tokenFactory.linkedTokenDeploySalt(wallet.address, salt);
});

it('Should revert on deploying an invalid token manager', async () => {
await expectRevert((gasOptions) => tokenFactory.linkToken(salt, '', token.address, 6, wallet.address, 0, gasOptions));
});

it('Should revert on deploying a local token manager with invalid params', async () => {
await expectRevert(
(gasOptions) => tokenFactory.linkToken(salt, '', token.address, NATIVE_INTERCHAIN_TOKEN, '0x', 0, gasOptions),
service,
'CannotDeploy',
);
});

it('Should revert on deploying a local token manager with interchain token manager type', async () => {
await expectRevert(
(gasOptions) => tokenFactory.linkToken(salt, '', token.address, NATIVE_INTERCHAIN_TOKEN, wallet.address, 0, gasOptions),
service,
'CannotDeploy',
[NATIVE_INTERCHAIN_TOKEN],
);
});

it('Should revert on deploying a remote token manager with interchain token manager type', async () => {
await expectRevert(
(gasOptions) =>
tokenFactory.linkToken(
salt,
destinationChain,
token.address,
NATIVE_INTERCHAIN_TOKEN,
wallet.address,
0,
gasOptions,
),
service,
'CannotDeploy',
[NATIVE_INTERCHAIN_TOKEN],
);
});

it('Should revert on deploying a token manager if token handler post deploy fails', async () => {
await expectRevert(
(gasOptions) => tokenFactory.linkToken(salt, '', AddressZero, LOCK_UNLOCK, wallet.address, 0, gasOptions),
service,
'PostDeployFailed',
);
});

it('Should deploy a lock_unlock token manager', async () => {
const tokenManagerAddress = await service.tokenManagerAddress(tokenId);
const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]);

await expect(
reportGas(
tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0),
'Call deployTokenManager on source chain',
),
)
.to.emit(service, 'InterchainTokenIdClaimed')
.withArgs(tokenId, AddressZero, factorySalt)
.to.emit(service, 'TokenManagerDeployed')
.withArgs(tokenId, tokenManagerAddress, LOCK_UNLOCK, params);

expect(tokenManagerAddress).to.not.equal(AddressZero);
const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet);

expect(await tokenManager.isOperator(wallet.address)).to.be.true;
expect(await tokenManager.isOperator(service.address)).to.be.true;
expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true;
expect(await tokenManager.isFlowLimiter(service.address)).to.be.true;

const tokenAddress = await service.registeredTokenAddress(tokenId);
expect(tokenAddress).to.eq(token.address);

tokenManagerProxy = await getContractAt('TokenManagerProxy', tokenManagerAddress, wallet);

const [implementation, tokenAddressFromProxy] = await tokenManagerProxy.getImplementationTypeAndTokenAddress();
expect(implementation).to.eq(LOCK_UNLOCK);
expect(tokenAddressFromProxy).to.eq(token.address);
});

it('Should revert when deploying a custom token manager twice', async () => {
const revertData = keccak256(toUtf8Bytes('AlreadyDeployed()')).substring(0, 10);
await expectRevert(
(gasOptions) => tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0, gasOptions),
service,
'TokenManagerDeploymentFailed',
[revertData],
);
});

it('Should revert when calling unsupported functions directly on the token manager implementation', async () => {
const implementationAddress = await tokenManagerProxy.implementation();
const implementationContract = await getContractAt('TokenManager', implementationAddress, wallet);
await expectRevert((gasOptions) => implementationContract.tokenAddress(gasOptions), implementationContract, 'NotSupported');
await expectRevert(
(gasOptions) => implementationContract.interchainTokenId(gasOptions),
implementationContract,
'NotSupported',
);
await expectRevert(
(gasOptions) => implementationContract.implementationType(gasOptions),
implementationContract,
'NotSupported',
);
});

it('Should deploy a mint_burn token manager', async () => {
const salt = getRandomBytes32();
const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt);
const tokenManagerAddress = await service.tokenManagerAddress(tokenId);
const token = await deployContract(wallet, 'TestInterchainTokenStandard', [
tokenName,
tokenSymbol,
tokenDecimals,
service.address,
tokenId,
]);
const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]);

const tx = tokenFactory.linkToken(salt, '', token.address, MINT_BURN, wallet.address, 0);
const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId);
await expect(tx).to.emit(service, 'TokenManagerDeployed').withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN, params);

expect(tokenManagerAddress).to.not.equal(AddressZero);
const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet);

expect(await tokenManager.isOperator(wallet.address)).to.be.true;
expect(await tokenManager.isOperator(service.address)).to.be.true;
expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true;
expect(await tokenManager.isFlowLimiter(service.address)).to.be.true;

const tokenAddress = await service.registeredTokenAddress(tokenId);
expect(tokenAddress).to.eq(token.address);

const tokenManagerProxy = await getContractAt('TokenManagerProxy', tokenManagerAddress, wallet);

const [implementation, tokenAddressFromProxy] = await tokenManagerProxy.getImplementationTypeAndTokenAddress();
expect(implementation).to.eq(MINT_BURN);
expect(tokenAddressFromProxy).to.eq(token.address);
});

it('Should deploy a mint_burn_from token manager', async () => {
const salt = getRandomBytes32();
const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt);
const tokenManagerAddress = await service.tokenManagerAddress(tokenId);
const token = await deployContract(wallet, 'TestInterchainTokenStandard', [
tokenName,
tokenSymbol,
tokenDecimals,
service.address,
tokenId,
]);
const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]);

const tx = tokenFactory.linkToken(salt, '', token.address, MINT_BURN_FROM, wallet.address, 0);
const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId);
await expect(tx)
.to.emit(service, 'TokenManagerDeployed')
.withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN_FROM, params);

expect(tokenManagerAddress).to.not.equal(AddressZero);
const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet);

expect(await tokenManager.isOperator(wallet.address)).to.be.true;
expect(await tokenManager.isOperator(service.address)).to.be.true;
expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true;
expect(await tokenManager.isFlowLimiter(service.address)).to.be.true;

const tokenAddress = await service.registeredTokenAddress(tokenId);
expect(tokenAddress).to.eq(token.address);

const tokenManagerProxy = await getContractAt('TokenManagerProxy', tokenManagerAddress, wallet);

const [implementation, tokenAddressFromProxy] = await tokenManagerProxy.getImplementationTypeAndTokenAddress();
expect(implementation).to.eq(MINT_BURN_FROM);
expect(tokenAddressFromProxy).to.eq(token.address);
});

it('Should deploy a lock_unlock_with_fee token manager', async () => {
const salt = getRandomBytes32();
const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt);
const tokenManagerAddress = await service.tokenManagerAddress(tokenId);
const token = await deployContract(wallet, 'TestFeeOnTransferToken', [
tokenName,
tokenSymbol,
tokenDecimals,
service.address,
tokenId,
]);
const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]);

const tx = tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK_FEE_ON_TRANSFER, wallet.address, 0);
const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId);
await expect(tx)
.to.emit(service, 'TokenManagerDeployed')
.withArgs(tokenId, expectedTokenManagerAddress, LOCK_UNLOCK_FEE_ON_TRANSFER, params);

expect(tokenManagerAddress).to.not.equal(AddressZero);
const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet);

expect(await tokenManager.isOperator(wallet.address)).to.be.true;
expect(await tokenManager.isOperator(service.address)).to.be.true;
expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true;
expect(await tokenManager.isFlowLimiter(service.address)).to.be.true;

const tokenAddress = await service.registeredTokenAddress(tokenId);
expect(tokenAddress).to.eq(token.address);

const tokenManagerProxy = await getContractAt('TokenManagerProxy', tokenManagerAddress, wallet);

const [implementation, tokenAddressFromProxy] = await tokenManagerProxy.getImplementationTypeAndTokenAddress();
expect(implementation).to.eq(LOCK_UNLOCK_FEE_ON_TRANSFER);
expect(tokenAddressFromProxy).to.eq(token.address);
});

it('Should revert when deploying a custom token manager if paused', async () => {
await service.setPauseStatus(true).then((tx) => tx.wait);

await expectRevert(
(gasOptions) => tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0, gasOptions),
service,
'Pause',
);

await service.setPauseStatus(false).then((tx) => tx.wait);
});
});
});
});