Skip to content

Commit

Permalink
feat: fee on transfer as a separate token manager (#96)
Browse files Browse the repository at this point in the history
* renamed folder and changed version

* npmignore

* npmignore

* change version

* using include pattern instead.

* Fixed most of the things least auhority suggested.

* made lint happy

* Apply suggestions from code review

* fixed some bugs

* added events

* rename set to transfer for distributor and operator

* changed standardized token to always allow token managers to mint/burn it.

* using immutable storage for remoteAddressValidator address to save gas

* Added some recommended changes

* added milap's suggested changes

* Fixed some names and some minor gas optimizations

* prettier and lint

* stash

* import .env in hardhat.config

* trying to fix .env.example

* Added some getters in IRemoteAddressValidator and removed useless check for distributor in the InterchainTokenService.

* removed ternary operators

* made lint happy

* made lint happy

* Added a new token manager to handle fee on transfer and added some tests for it as well

* fixed the liquidity pool check.

* fix a duplication bug

* lint

* added some more tests

* Added more tests

* Added proper re-entrancy protection for fee on transfer token managers.

* change to tx.origin for refunds

* Added support for more kinds of addresses.

* some minor gas opts

* some more gas optimizations.

---------

Co-authored-by: Milap Sheth <[email protected]>
  • Loading branch information
Foivos and milapsheth authored Sep 4, 2023
1 parent c04aaa3 commit 8d66c0b
Show file tree
Hide file tree
Showing 13 changed files with 387 additions and 58 deletions.
32 changes: 16 additions & 16 deletions contracts/interchain-token-service/InterchainTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ contract InterchainTokenService is

address internal immutable implementationLockUnlock;
address internal immutable implementationMintBurn;
address internal immutable implementationLockUnlockFee;
address internal immutable implementationLiquidityPool;
IAxelarGasService public immutable gasService;
IRemoteAddressValidator public immutable remoteAddressValidator;
Expand Down Expand Up @@ -103,6 +104,10 @@ contract InterchainTokenService is

implementationLockUnlock = _sanitizeTokenManagerImplementation(tokenManagerImplementations, TokenManagerType.LOCK_UNLOCK);
implementationMintBurn = _sanitizeTokenManagerImplementation(tokenManagerImplementations, TokenManagerType.MINT_BURN);
implementationLockUnlockFee = _sanitizeTokenManagerImplementation(
tokenManagerImplementations,
TokenManagerType.LOCK_UNLOCK_FEE_ON_TRANSFER
);
implementationLiquidityPool = _sanitizeTokenManagerImplementation(tokenManagerImplementations, TokenManagerType.LIQUIDITY_POOL);

chainName = chainName_.toBytes32();
Expand Down Expand Up @@ -143,14 +148,6 @@ contract InterchainTokenService is
return CONTRACT_ID;
}

/**
* @notice Getter for the chain name.
* @return name the name of the chain
*/
function getChainName() public view returns (string memory name) {
name = chainName.toTrimmedString();
}

/**
* @notice Calculates the address of a TokenManager from a specific tokenId. The TokenManager does not need to exist already.
* @param tokenId the tokenId.
Expand Down Expand Up @@ -225,6 +222,8 @@ contract InterchainTokenService is
return implementationLockUnlock;
} else if (TokenManagerType(tokenManagerType) == TokenManagerType.MINT_BURN) {
return implementationMintBurn;
} else if (TokenManagerType(tokenManagerType) == TokenManagerType.LOCK_UNLOCK_FEE_ON_TRANSFER) {
return implementationLockUnlockFee;
} else if (TokenManagerType(tokenManagerType) == TokenManagerType.LIQUIDITY_POOL) {
return implementationLiquidityPool;
}
Expand Down Expand Up @@ -439,6 +438,7 @@ contract InterchainTokenService is
/**
* @notice Uses the caller's tokens to fullfill a sendCall ahead of time. Use this only if you have detected an outgoing
* sendToken that matches the parameters passed here.
* @dev This is not to be used with fee on transfer tokens as it will incur losses for the express caller.
* @param tokenId the tokenId of the TokenManager used.
* @param destinationAddress the destinationAddress for the sendToken.
* @param amount the amount of token to give.
Expand All @@ -459,6 +459,7 @@ contract InterchainTokenService is
/**
* @notice Uses the caller's tokens to fullfill a callContractWithInterchainToken ahead of time. Use this only if you have
* detected an outgoing sendToken that matches the parameters passed here.
* @dev This is not to be used with fee on transfer tokens as it will incur losses for the express caller and it will pass an incorrect amount to the contract.
* @param tokenId the tokenId of the TokenManager used.
* @param sourceChain the name of the chain where the call came from.
* @param sourceAddress the caller of callContractWithInterchainToken.
Expand Down Expand Up @@ -513,15 +514,15 @@ contract InterchainTokenService is
bytes memory payload;
if (metadata.length < 4) {
payload = abi.encode(SELECTOR_SEND_TOKEN, tokenId, destinationAddress, amount);
_callContract(destinationChain, payload, msg.value, sourceAddress);
_callContract(destinationChain, payload, msg.value);
emit TokenSent(tokenId, destinationChain, destinationAddress, amount);
return;
}
uint32 version;
(version, metadata) = _decodeMetadata(metadata);
if (version > 0) revert InvalidMetadataVersion(version);
payload = abi.encode(SELECTOR_SEND_TOKEN_WITH_DATA, tokenId, destinationAddress, amount, sourceAddress.toBytes(), metadata);
_callContract(destinationChain, payload, msg.value, sourceAddress);
_callContract(destinationChain, payload, msg.value);
emit TokenSentWithData(tokenId, destinationChain, destinationAddress, amount, sourceAddress, metadata);
}

Expand Down Expand Up @@ -719,17 +720,16 @@ contract InterchainTokenService is
* @param destinationChain The target chain where the contract will be called
* @param payload The data payload for the transaction
* @param gasValue The amount of gas to be paid for the transaction
* @param refundTo The address where the unused gas amount should be refunded to
*/
function _callContract(string calldata destinationChain, bytes memory payload, uint256 gasValue, address refundTo) internal {
function _callContract(string calldata destinationChain, bytes memory payload, uint256 gasValue) internal {
string memory destinationAddress = remoteAddressValidator.getRemoteAddress(destinationChain);
if (gasValue > 0) {
gasService.payNativeGasForContractCall{ value: gasValue }(
address(this),
destinationChain,
destinationAddress,
payload,
refundTo
payload, // solhint-disable-next-line avoid-tx-origin
tx.origin
);
}
gateway.callContract(destinationChain, destinationAddress, payload);
Expand Down Expand Up @@ -758,7 +758,7 @@ contract InterchainTokenService is
bytes memory params
) internal {
bytes memory payload = abi.encode(SELECTOR_DEPLOY_TOKEN_MANAGER, tokenId, tokenManagerType, params);
_callContract(destinationChain, payload, gasValue, msg.sender);
_callContract(destinationChain, payload, gasValue);
emit RemoteTokenManagerDeploymentInitialized(tokenId, destinationChain, gasValue, tokenManagerType, params);
}

Expand Down Expand Up @@ -798,7 +798,7 @@ contract InterchainTokenService is
mintAmount,
operator
);
_callContract(destinationChain, payload, gasValue, msg.sender);
_callContract(destinationChain, payload, gasValue);
emit RemoteStandardizedTokenAndManagerDeploymentInitialized(
tokenId,
name,
Expand Down
6 changes: 0 additions & 6 deletions contracts/interfaces/IInterchainTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,6 @@ interface IInterchainTokenService is ITokenManagerType, IExpressCallHandler, IAx
*/
function standardizedTokenDeployer() external view returns (address standardizedTokenDeployerAddress);

/**
* @notice Returns the name of the current chain.
* @return name The name of the current chain.
*/
function getChainName() external view returns (string memory name);

/**
* @notice Returns the address of the token manager associated with the given tokenId.
* @param tokenId The tokenId of the token manager.
Expand Down
12 changes: 12 additions & 0 deletions contracts/interfaces/INoReEntrancy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
* @title Pausable
* @notice This contract provides a mechanism to halt the execution of specific functions
* if a pause condition is activated.
*/
interface INoReEntrancy {
error ReEntrancy();
}
1 change: 1 addition & 0 deletions contracts/interfaces/ITokenManagerType.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ITokenManagerType {
enum TokenManagerType {
LOCK_UNLOCK,
MINT_BURN,
LOCK_UNLOCK_FEE_ON_TRANSFER,
LIQUIDITY_POOL
}
}
15 changes: 8 additions & 7 deletions contracts/remote-address-validator/RemoteAddressValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ contract RemoteAddressValidator is IRemoteAddressValidator, Upgradable {
*/
function _lowerCase(string memory s) internal pure returns (string memory) {
uint256 length = bytes(s).length;

for (uint256 i; i < length; i++) {
uint8 b = uint8(bytes(s)[i]);
if ((b >= 65) && (b <= 70)) bytes(s)[i] = bytes1(b + uint8(32));
uint8 b;
for (uint256 i; i < length; ++i) {
b = uint8(bytes(s)[i]);
if ((b >= 65) && (b <= 90)) bytes(s)[i] = bytes1(b + uint8(32));
}

return s;
Expand Down Expand Up @@ -152,9 +152,9 @@ contract RemoteAddressValidator is IRemoteAddressValidator, Upgradable {
*/
function addGatewaySupportedChains(string[] calldata chainNames) external onlyOwner {
uint256 length = chainNames.length;

string calldata chainName;
for (uint256 i; i < length; ++i) {
string calldata chainName = chainNames[i];
chainName = chainNames[i];
supportedByGateway[chainName] = true;

emit GatewaySupportedChainAdded(chainName);
Expand All @@ -167,9 +167,10 @@ contract RemoteAddressValidator is IRemoteAddressValidator, Upgradable {
*/
function removeGatewaySupportedChains(string[] calldata chainNames) external onlyOwner {
uint256 length = chainNames.length;
string calldata chainName;

for (uint256 i; i < length; ++i) {
string calldata chainName = chainNames[i];
chainName = chainNames[i];
supportedByGateway[chainName] = false;

emit GatewaySupportedChainRemoved(chainName);
Expand Down
72 changes: 72 additions & 0 deletions contracts/test/FeeOnTransferTokenTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { InterchainToken } from '../interchain-token/InterchainToken.sol';
import { Distributable } from '../utils/Distributable.sol';
import { ITokenManager } from '../interfaces/ITokenManager.sol';
import { IERC20BurnableMintable } from '../interfaces/IERC20BurnableMintable.sol';

contract FeeOnTransferTokenTest is InterchainToken, Distributable, IERC20BurnableMintable {
ITokenManager public tokenManager_;
bool internal tokenManagerRequiresApproval_ = true;
string public name;
string public symbol;
uint8 public decimals;

constructor(string memory name_, string memory symbol_, uint8 decimals_, address tokenManagerAddress) {
name = name_;
symbol = symbol_;
decimals = decimals_;
_setDistributor(msg.sender);
tokenManager_ = ITokenManager(tokenManagerAddress);
}

function tokenManager() public view override returns (ITokenManager) {
return tokenManager_;
}

function _beforeInterchainTransfer(
address sender,
string calldata /*destinationChain*/,
bytes calldata /*destinationAddress*/,
uint256 amount,
bytes calldata /*metadata*/
) internal override {
if (!tokenManagerRequiresApproval_) return;
address tokenManagerAddress = address(tokenManager_);
uint256 allowance_ = allowance[sender][tokenManagerAddress];
if (allowance_ != type(uint256).max) {
if (allowance_ > type(uint256).max - amount) {
allowance_ = type(uint256).max - amount;
}

_approve(sender, tokenManagerAddress, allowance_ + amount);
}
}

function setTokenManagerRequiresApproval(bool requiresApproval) public {
tokenManagerRequiresApproval_ = requiresApproval;
}

function mint(address account, uint256 amount) external onlyDistributor {
_mint(account, amount);
}

function burn(address account, uint256 amount) external onlyDistributor {
_burn(account, amount);
}

function setTokenManager(ITokenManager tokenManagerAddress) external {
tokenManager_ = tokenManagerAddress;
}

// Always transfer 10 less base tokens.
function _transfer(address sender, address recipient, uint256 amount) internal override {
if (sender == address(0) || recipient == address(0)) revert InvalidAccount();

balanceOf[sender] -= amount;
balanceOf[recipient] += amount - 10;
emit Transfer(sender, recipient, amount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pragma solidity ^0.8.0;

import { TokenManagerAddressStorage } from './TokenManagerAddressStorage.sol';
import { NoReEntrancy } from '../../utils/NoReEntrancy.sol';
import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol';

import { SafeTokenTransferFrom } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol';
Expand All @@ -14,7 +15,7 @@ import { SafeTokenTransferFrom } from '@axelar-network/axelar-gmp-sdk-solidity/c
* @dev This contract extends TokenManagerAddressStorage and provides implementation for its abstract methods.
* It uses the Axelar SDK to safely transfer tokens.
*/
contract TokenManagerLiquidityPool is TokenManagerAddressStorage {
contract TokenManagerLiquidityPool is TokenManagerAddressStorage, NoReEntrancy {
// uint256(keccak256('liquidity-pool-slot')) - 1
uint256 internal constant LIQUIDITY_POOL_SLOT = 0x8e02741a3381812d092c5689c9fc701c5185c1742fdf7954c4c4472be4cc4807;

Expand All @@ -26,7 +27,7 @@ contract TokenManagerLiquidityPool is TokenManagerAddressStorage {
constructor(address interchainTokenService_) TokenManagerAddressStorage(interchainTokenService_) {}

function implementationType() external pure returns (uint256) {
return 2;
return 3;
}

/**
Expand Down Expand Up @@ -74,7 +75,7 @@ contract TokenManagerLiquidityPool is TokenManagerAddressStorage {
* @param amount The amount of tokens to transfer
* @return uint The actual amount of tokens transferred. This allows support for fee-on-transfer tokens.
*/
function _takeToken(address from, uint256 amount) internal override returns (uint256) {
function _takeToken(address from, uint256 amount) internal override noReEntrancy returns (uint256) {
IERC20 token = IERC20(tokenAddress());
address liquidityPool_ = liquidityPool();
uint256 balance = token.balanceOf(liquidityPool_);
Expand All @@ -91,7 +92,7 @@ contract TokenManagerLiquidityPool is TokenManagerAddressStorage {
* @param amount The amount of tokens to transfer
* @return uint The actual amount of tokens transferred
*/
function _giveToken(address to, uint256 amount) internal override returns (uint256) {
function _giveToken(address to, uint256 amount) internal override noReEntrancy returns (uint256) {
IERC20 token = IERC20(tokenAddress());
uint256 balance = IERC20(token).balanceOf(to);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,10 @@ contract TokenManagerLockUnlock is TokenManagerAddressStorage {
*/
function _takeToken(address from, uint256 amount) internal override returns (uint256) {
IERC20 token = IERC20(tokenAddress());
uint256 balance = token.balanceOf(address(this));

SafeTokenTransferFrom.safeTransferFrom(token, from, address(this), amount);

// Note: This allows support for fee-on-transfer tokens
return IERC20(token).balanceOf(address(this)) - balance;
return amount;
}

/**
Expand All @@ -59,10 +57,9 @@ contract TokenManagerLockUnlock is TokenManagerAddressStorage {
*/
function _giveToken(address to, uint256 amount) internal override returns (uint256) {
IERC20 token = IERC20(tokenAddress());
uint256 balance = IERC20(token).balanceOf(to);

SafeTokenTransfer.safeTransfer(token, to, amount);

return IERC20(token).balanceOf(to) - balance;
return amount;
}
}
Loading

0 comments on commit 8d66c0b

Please sign in to comment.