Skip to content

Commit

Permalink
feat: clone instead of fixed proxy (#168)
Browse files Browse the repository at this point in the history
* Using minimal proxy to hopefully save some gas.

* testing gas

* remove gas test

* prettier

* prettier

* added some comments

* addressed some comments

* fixed tests and prettier

* clean up tests

* address comments

* lint

---------

Co-authored-by: Milap Sheth <[email protected]>
  • Loading branch information
Foivos and milapsheth authored Nov 13, 2023
1 parent 9954e3f commit 74e2524
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 138 deletions.
53 changes: 39 additions & 14 deletions contracts/interchain-token/InterchainToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
pragma solidity ^0.8.0;

import { AddressBytes } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/AddressBytes.sol';
import { IImplementation } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IImplementation.sol';
import { Implementation } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Implementation.sol';

import { IInterchainToken } from '../interfaces/IInterchainToken.sol';

Expand All @@ -17,21 +15,38 @@ import { Distributable } from '../utils/Distributable.sol';
* @notice This contract implements a interchain token which extends InterchainToken functionality.
* This contract also inherits Distributable and Implementation logic.
*/
contract InterchainToken is BaseInterchainToken, ERC20Permit, Implementation, Distributable, IInterchainToken {
contract InterchainToken is BaseInterchainToken, ERC20Permit, Distributable, IInterchainToken {
using AddressBytes for bytes;

string public name;
string public symbol;
uint8 public decimals;
address internal tokenManager_;

bytes32 private constant CONTRACT_ID = keccak256('interchain-token');
// bytes32(uint256(keccak256('interchain-token-initialized')) - 1);
bytes32 internal constant INITIALIZED_SLOT = 0xc778385ecb3e8cecb82223fa1f343ec6865b2d64c65b0c15c7e8aef225d9e214;

constructor() {
// Make the implementation act as if it has been setup already to disallow calls to init() (even though that wouldn't achieve anything really)
_initialize();
}

/**
* @notice Getter for the contract id.
* @notice returns true if the contract has be setup.
*/
function contractId() external pure override returns (bytes32) {
return CONTRACT_ID;
function _isInitialized() internal view returns (bool initialized) {
assembly {
initialized := sload(INITIALIZED_SLOT)
}
}

/**
* @notice sets initialized to true, to allow only a single init.
*/
function _initialize() internal {
assembly {
sstore(INITIALIZED_SLOT, true)
}
}

/**
Expand All @@ -44,20 +59,30 @@ contract InterchainToken is BaseInterchainToken, ERC20Permit, Implementation, Di

/**
* @notice Setup function to initialize contract parameters
* @param params The setup parameters in bytes
* The setup params include tokenManager, distributor, tokenName, symbol, decimals, mintAmount and mintTo
* @param tokenManagerAddress The address of the token manager of this token
* @param distributor The address of the token distributor
* @param tokenName The name of the token
* @param tokenSymbol The symbopl of the token
* @param tokenDecimals The decimals of the token
*/
function setup(bytes calldata params) external override(Implementation, IImplementation) onlyProxy {
address distributor;
address tokenManagerAddress;
string memory tokenName;
(tokenManagerAddress, distributor, tokenName, symbol, decimals) = abi.decode(params, (address, address, string, string, uint8));
function init(
address tokenManagerAddress,
address distributor,
string calldata tokenName,
string calldata tokenSymbol,
uint8 tokenDecimals
) external {
if (_isInitialized()) revert AlreadyInitialized();

_initialize();

if (tokenManagerAddress == address(0)) revert TokenManagerAddressZero();
if (bytes(tokenName).length == 0) revert TokenNameEmpty();

tokenManager_ = tokenManagerAddress;
name = tokenName;
symbol = tokenSymbol;
decimals = tokenDecimals;

if (distributor != address(0)) _addDistributor(distributor);
_addDistributor(tokenManagerAddress);
Expand Down
21 changes: 18 additions & 3 deletions contracts/interfaces/IInterchainToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

pragma solidity ^0.8.0;

import { IImplementation } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IImplementation.sol';

import { IInterchainTokenStandard } from './IInterchainTokenStandard.sol';
import { IDistributable } from './IDistributable.sol';
import { IERC20MintableBurnable } from './IERC20MintableBurnable.sol';
Expand All @@ -12,14 +10,31 @@ import { IERC20Named } from './IERC20Named.sol';
/**
* @title IInterchainToken
*/
interface IInterchainToken is IInterchainTokenStandard, IDistributable, IERC20MintableBurnable, IERC20Named, IImplementation {
interface IInterchainToken is IInterchainTokenStandard, IDistributable, IERC20MintableBurnable, IERC20Named {
error TokenManagerAddressZero();
error TokenNameEmpty();
error AlreadyInitialized();

/**
* @notice Getter for the tokenManager used for this token.
* @dev Needs to be overwitten.
* @return tokenManager_ the TokenManager called to facilitate cross chain transfers.
*/
function tokenManager() external view returns (address tokenManager_);

/**
* @notice Setup function to initialize contract parameters
* @param tokenManagerAddress The address of the token manager of this token
* @param distributor The address of the token distributor
* @param tokenName The name of the token
* @param tokenSymbol The symbopl of the token
* @param tokenDecimals The decimals of the token
*/
function init(
address tokenManagerAddress,
address distributor,
string calldata tokenName,
string calldata tokenSymbol,
uint8 tokenDecimals
) external;
}
2 changes: 1 addition & 1 deletion contracts/interfaces/IInterchainTokenDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ interface IInterchainTokenDeployer {
string calldata name,
string calldata symbol,
uint8 decimals
) external payable returns (address tokenAddress);
) external returns (address tokenAddress);
}
33 changes: 0 additions & 33 deletions contracts/proxies/InterchainTokenProxy.sol

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BaseInterchainToken } from '../interchain-token/BaseInterchainToken.sol
import { Distributable } from '../utils/Distributable.sol';
import { IERC20MintableBurnable } from '../interfaces/IERC20MintableBurnable.sol';

contract InterchainTokenTest is BaseInterchainToken, Distributable, IERC20MintableBurnable {
contract BaseInterchainTokenTest is BaseInterchainToken, Distributable, IERC20MintableBurnable {
address public tokenManager_;
bool internal tokenManagerRequiresApproval_ = true;
string public name;
Expand Down
21 changes: 8 additions & 13 deletions contracts/test/HardCodedConstantsTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,31 @@
pragma solidity ^0.8.0;

import { TokenManagerLiquidityPool } from '../token-manager/TokenManagerLiquidityPool.sol';
import { Distributable } from '../utils/Distributable.sol';
import { FlowLimit } from '../utils/FlowLimit.sol';
import { Operatable } from '../utils/Operatable.sol';
import { InterchainToken } from '../interchain-token/InterchainToken.sol';

error Invalid();

contract TestTokenManager is TokenManagerLiquidityPool {
string public constant NAME = 'TestTokenManager';
string public placeholder;

constructor(address interchainTokenService_) TokenManagerLiquidityPool(interchainTokenService_) {
if (LIQUIDITY_POOL_SLOT != uint256(keccak256('liquidity-pool-slot')) - 1) revert Invalid();
}
}

contract TestDistributable is Distributable {
string public constant NAME = 'TestDistributable';

constructor() {}
}

contract TestFlowLimit is FlowLimit {
string public constant NAME = 'TestFlowLimit';
string public placeholder;

constructor() {
if (FLOW_LIMIT_SLOT != uint256(keccak256('flow-limit')) - 1) revert Invalid();
}
}

contract TestOperatable is Operatable {
string public constant NAME = 'TestOperatable';
contract TestInterchainToken is InterchainToken {
string public placeholder;

constructor() {}
constructor() {
if (INITIALIZED_SLOT != bytes32(uint256(keccak256('interchain-token-initialized')) - 1)) revert Invalid();
}
}
2 changes: 1 addition & 1 deletion contracts/test/utils/OperatableTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.0;

import { Operatable } from '../../utils/Operatable.sol';

contract OperatorableTest is Operatable {
contract OperatableTest is Operatable {
uint256 public nonce;

constructor(address operator) {
Expand Down
27 changes: 20 additions & 7 deletions contracts/utils/InterchainTokenDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ pragma solidity ^0.8.0;
import { Create3 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/deploy/Create3.sol';

import { IInterchainTokenDeployer } from '../interfaces/IInterchainTokenDeployer.sol';

import { InterchainTokenProxy } from '../proxies/InterchainTokenProxy.sol';
import { IInterchainToken } from '../interfaces/IInterchainToken.sol';

/**
* @title InterchainTokenDeployer
Expand All @@ -21,6 +20,7 @@ contract InterchainTokenDeployer is IInterchainTokenDeployer, Create3 {
*/
constructor(address implementationAddress_) {
if (implementationAddress_ == address(0)) revert AddressZero();

implementationAddress = implementationAddress_;
}

Expand All @@ -34,21 +34,34 @@ contract InterchainTokenDeployer is IInterchainTokenDeployer, Create3 {
* @param decimals Decimals of the token
* @return tokenAddress Address of the deployed token
*/
// slither-disable-next-line locked-ether
function deployInterchainToken(
bytes32 salt,
address tokenManager,
address distributor,
string calldata name,
string calldata symbol,
uint8 decimals
) external payable returns (address tokenAddress) {
bytes memory params = abi.encode(tokenManager, distributor, name, symbol, decimals);
// slither-disable-next-line too-many-digits
bytes memory bytecode = bytes.concat(type(InterchainTokenProxy).creationCode, abi.encode(implementationAddress, params));
) external returns (address tokenAddress) {
// The minimal proxy bytecode is the same as https://github.com/OpenZeppelin/openzeppelin-contracts/blob/94697be8a3f0dfcd95dfb13ffbd39b5973f5c65d/contracts/proxy/Clones.sol#L28
// The minimal proxy bytecode is 0x37 = 55 bytes long
bytes memory bytecode = new bytes(0x37);
address implementation = implementationAddress;

/// @solidity memory-safe-assembly
assembly {
// The first 0x20 = 32 bytes (0x00 - 0x19) are reserved for the length.
// The next 0x14 = 20 bytes (0x20 - 0x33) are the ones below.
mstore(add(bytecode, 0x20), shl(0x60, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73))
// The next 0x14 = 20 bytes (0x34 - 0x47) are the implementation address.
mstore(add(bytecode, 0x34), shl(0x60, implementation))
// The last 0x0f = 15 bytes (0x48 - 0x56) are the ones below.
mstore(add(bytecode, 0x48), shl(0x88, 0x5af43d82803e903d91602b57fd5bf3))
}

tokenAddress = _create3(bytecode, salt);
if (tokenAddress.code.length == 0) revert TokenDeploymentFailed();

IInterchainToken(tokenAddress).init(tokenManager, distributor, name, symbol, decimals);
}

function deployedAddress(bytes32 salt) external view returns (address tokenAddress) {
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3501,7 +3501,7 @@ string lastMessage
function _executeWithInterchainToken(string sourceChain, bytes sourceAddress, bytes data, bytes32 tokenId, address token, uint256 amount) internal
```

## InterchainTokenTest
## BaseInterchainTokenTest

### tokenManager_

Expand Down Expand Up @@ -3791,7 +3791,7 @@ fallback() external payable virtual
receive() external payable virtual
```

## OperatorableTest
## OperatableTest

### nonce

Expand Down
55 changes: 14 additions & 41 deletions test/InterchainToken.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
'use strict';

const chai = require('chai');
const { ethers } = require('hardhat');
const {
getContractAt,
utils: { keccak256, toUtf8Bytes },
} = ethers;
const { expect } = chai;
const { getRandomBytes32, expectRevert, getGasOptions } = require('./utils');
const { getContractAt } = ethers;
const { getRandomBytes32, expectRevert } = require('./utils');
const { deployContract } = require('../scripts/deploy');

describe('InterchainToken', () => {
Expand All @@ -19,7 +14,6 @@ describe('InterchainToken', () => {
const mintAmount = 123;

let token;
let tokenProxy;

let owner;

Expand All @@ -35,7 +29,6 @@ describe('InterchainToken', () => {
const tokenAddress = await interchainTokenDeployer.deployedAddress(salt);

token = await getContractAt('InterchainToken', tokenAddress, owner);
tokenProxy = await getContractAt('InterchainTokenProxy', tokenAddress, owner);

await interchainTokenDeployer
.deployInterchainToken(salt, owner.address, owner.address, name, symbol, decimals)
Expand All @@ -44,41 +37,21 @@ describe('InterchainToken', () => {
await (await token.mint(owner.address, mintAmount)).wait();
});

describe('Interchain Token Proxy', () => {
it('should revert if interchain token implementation is invalid', async () => {
const invalidInterchainToken = await deployContract(owner, 'InvalidInterchainToken');
interchainTokenDeployer = await deployContract(owner, 'InterchainTokenDeployer', [invalidInterchainToken.address]);

const salt = getRandomBytes32();

await expect(
interchainTokenDeployer.deployInterchainToken(salt, owner.address, owner.address, name, symbol, decimals, getGasOptions()),
).to.be.reverted;
});

it('should revert if interchain token setup fails', async () => {
const params = '0x1234';
await expectRevert(
(gasOptions) => deployContract(owner, 'InterchainTokenProxy', [interchainToken.address, params, gasOptions]),
tokenProxy,
'SetupFailed',
);
});

it('should return the correct contract ID', async () => {
const contractID = await token.contractId();
const hash = keccak256(toUtf8Bytes('interchain-token'));
expect(contractID).to.equal(hash);
});
});

describe('Interchain Token', () => {
it('revert on setup if not called by the proxy', async () => {
const implementationAddress = await tokenProxy.implementation();
it('revert on init if not called by the proxy', async () => {
const implementationAddress = await interchainTokenDeployer.implementationAddress();
const implementation = await getContractAt('InterchainToken', implementationAddress, owner);

const params = '0x';
await expectRevert((gasOptions) => implementation.setup(params, gasOptions), token, 'NotProxy');
const tokenManagerAddress = owner.address;
const distributor = owner.address;
const tokenName = 'name';
const tokenSymbol = 'symbol';
const tokenDecimals = 7;
await expectRevert(
(gasOptions) => implementation.init(tokenManagerAddress, distributor, tokenName, tokenSymbol, tokenDecimals, gasOptions),
implementation,
'AlreadyInitialized',
);
});
});
});
Loading

0 comments on commit 74e2524

Please sign in to comment.