Skip to content
Merged
8 changes: 8 additions & 0 deletions chains/evm/contracts/TokenPoolFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ contract TokenPoolFactory is ITypeAndVersion {
/// remote token pools. The token pool is then set in the token admin registry. Ownership of the everything is transferred
/// to the futureOwner (or msg.sender if no futureOwner is given), but must be accepted in a separate transaction due
/// to 2-step ownership transfer.
/// @dev Not all tokens are supported. This factory is designed to work with the CrossChainToken, and while other
/// tokens may be compatible, no guarantees are given. The factory does not verify compatibility, so using an
/// incompatible token may result in a broken deployment.
/// @param remoteTokenPools An array of remote token pools info to be used in the pool's applyChainUpdates function
/// or to be predicted if the pool has not been deployed yet on the remote chain.
/// @param localTokenDecimals The amount of decimals to be used in the new token. Since decimals() is not part of the
Expand All @@ -126,6 +129,9 @@ contract TokenPoolFactory is ITypeAndVersion {
/// @param tokenPoolInitCode The creation code for the token pool, without the constructor parameters appended.
/// @param lockBox The lockbox associated with the token, required for lock/release pools.
/// @param salt The salt to be used in the create2 deployment of the token and token pool to ensure a unique address.
/// @param futureOwner The address that will own the token and pool after the transaction. If set to address(0), the
/// sender will be the future owner. If the token mints any funds to the factory, the factory will forward these to
/// the futureOwner.
/// @return token The address of the token that was deployed.
/// @return pool The address of the token pool that was deployed.
function deployTokenAndTokenPool(
Expand Down Expand Up @@ -187,6 +193,8 @@ contract TokenPoolFactory is ITypeAndVersion {
/// @param tokenPoolInitCode The creation code for the token pool.
/// @param lockBox The lockbox associated with the token, required for lock/release pools.
/// @param salt The salt to be used in the create2 deployment of the token pool.
/// @param futureOwner The address that will own the pool after the transaction. If set to address(0), the sender will
/// be the future owner.
/// @return poolAddress The address of the token pool that was deployed.
function deployTokenPoolWithExistingToken(
address token,
Expand Down
21 changes: 19 additions & 2 deletions chains/evm/contracts/pools/CrossChainPoolToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {TokenPool} from "./TokenPool.sol";
import {ERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol";

/// @notice A CCIP token pool that is also an ERC20 token. This allows the pool to burn/mint without needing to manage
/// roles for a separate token contract.
/// @dev This contract inherits its access control from TokenPool, meaning it uses an `owner` role with 2-step ownership
/// transfers. There's also a separate `ccipAdmin` role which can be used to register with the CCIP token admin registry
/// but has no other special powers, and can only be transferred by the owner. The owner role can also be used to
/// register the token in the token admin registry.
contract CrossChainPoolToken is TokenPool, BaseERC20 {
function typeAndVersion() external pure virtual override returns (string memory) {
return "CrossChainPoolToken 2.0.0-dev";
Expand All @@ -24,36 +30,47 @@ contract CrossChainPoolToken is TokenPool, BaseERC20 {

/// @notice Burns tokens held by the pool. The Router transfers tokens to
/// this contract before the OnRamp calls lockOrBurn, so the burn is from self.
/// @param amount The amount of tokens to burn.
function _lockOrBurn(
uint64, // remoteChainSelector
uint256 amount
) internal virtual override {
_burn(address(this), amount);
}

/// @notice Mints tokens to the receiver.
/// @param receiver The address to mint tokens to.
/// @param amount The amount of tokens to mint.
function _releaseOrMint(
address receiver,
uint256 amount,
uint64 // remoteChainSelector
) internal virtual override {
if (i_maxSupply != 0 && totalSupply() + amount > i_maxSupply) revert MaxSupplyExceeded(totalSupply() + amount);

_mint(receiver, amount);
}

/// @dev Overrides BaseERC20._update to allow this contract to receive its own tokens.
/// The CCIP Router transfers tokens to the pool (which IS this contract) before
/// lockOrBurn is called, so transfers to address(this) must be permitted.
/// @dev This function must reflect any changes made in BaseERC20._update, which it currently does by adding the
/// supply check.
function _update(
address from,
address to,
uint256 value
) internal virtual override {
// Update first, then check the total supply.
ERC20._update(from, to, value);

// If `from` is address(0), this is a mint, so we need to check the total supply against the max supply.
if (from == address(0)) {
_assertMaxSupply();
}
}

/// @notice Signals which version of the pool interface is supported.
/// @param interfaceId The interface identifier, as specified in ERC-165.
/// @return True if the contract implements the requested interface, false otherwise.
function supportsInterface(
bytes4 interfaceId
) public view virtual override(BaseERC20, TokenPool) returns (bool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ contract CrossChainPoolToken_releaseOrMint is CrossChainPoolTokenSetup {
vm.startPrank(s_allowedOffRamp);

vm.expectRevert(
abi.encodeWithSelector(BaseERC20.MaxSupplyExceeded.selector, IERC20(address(s_cctPool)).totalSupply() + tooMuch)
abi.encodeWithSelector(
BaseERC20.MaxSupplyExceeded.selector, IERC20(address(s_cctPool)).totalSupply() + tooMuch, MAX_SUPPLY
)
);
s_cctPool.releaseOrMint(
Pool.ReleaseOrMintInV1({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ contract BaseERC20_constructor is BaseERC20Setup {
uint256 maxSupply = 500e18;
uint256 preMint = maxSupply + 1;

vm.expectRevert(abi.encodeWithSelector(BaseERC20.MaxSupplyExceeded.selector, preMint));
vm.expectRevert(abi.encodeWithSelector(BaseERC20.MaxSupplyExceeded.selector, preMint, maxSupply));
new BaseERC20(
BaseERC20.ConstructorParams({
name: "Over",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ contract BaseERC20_setCCIPAdmin is BaseERC20Setup {
vm.expectRevert(BaseERC20.OnlyCCIPAdmin.selector);
s_baseERC20.setCCIPAdmin(STRANGER);
}

function test_setCCIPAdmin_RevertWhen_CannotRenounceCCIPAdmin() public {
vm.expectRevert(BaseERC20.CannotRenounceCCIPAdmin.selector);
s_baseERC20.setCCIPAdmin(address(0));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

import {BaseERC20} from "../../../tokens/BaseERC20.sol";
import {BaseERC20Setup} from "./BaseERC20Setup.t.sol";
import {IERC20Errors} from "@openzeppelin/contracts@5.3.0/interfaces/draft-IERC6093.sol";

contract BaseERC20_transfer is BaseERC20Setup {
function test_transfer() public {
Expand All @@ -18,12 +18,12 @@ contract BaseERC20_transfer is BaseERC20Setup {
// Reverts

function test_transfer_RevertWhen_InvalidRecipient_TransferToSelf() public {
vm.expectRevert(abi.encodeWithSelector(BaseERC20.InvalidRecipient.selector, address(s_baseERC20)));
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidReceiver.selector, address(s_baseERC20)));
s_baseERC20.transfer(address(s_baseERC20), 100e18);
}

function test_approve_RevertWhen_InvalidRecipient_ApproveToSelf() public {
vm.expectRevert(abi.encodeWithSelector(BaseERC20.InvalidRecipient.selector, address(s_baseERC20)));
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidSpender.selector, address(s_baseERC20)));
s_baseERC20.approve(address(s_baseERC20), 100e18);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {BaseERC20} from "../../../tokens/BaseERC20.sol";
import {CrossChainTokenSetup} from "./CrossChainTokenSetup.t.sol";

import {IAccessControl} from "@openzeppelin/contracts@5.3.0/access/IAccessControl.sol";
import {IERC20Errors} from "@openzeppelin/contracts@5.3.0/interfaces/draft-IERC6093.sol";
import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol";

contract CrossChainToken_mint is CrossChainTokenSetup {
Expand Down Expand Up @@ -41,15 +42,17 @@ contract CrossChainToken_mint is CrossChainTokenSetup {
vm.startPrank(s_minter);

vm.expectRevert(
abi.encodeWithSelector(BaseERC20.MaxSupplyExceeded.selector, s_crossChainToken.totalSupply() + remaining + 1)
abi.encodeWithSelector(
BaseERC20.MaxSupplyExceeded.selector, s_crossChainToken.totalSupply() + remaining + 1, MAX_SUPPLY
)
);
s_crossChainToken.mint(makeAddr("receiver"), remaining + 1);
}

function test_mint_RevertWhen_InvalidRecipient() public {
vm.startPrank(s_minter);

vm.expectRevert(abi.encodeWithSelector(BaseERC20.InvalidRecipient.selector, address(s_crossChainToken)));
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidReceiver.selector, address(s_crossChainToken)));
s_crossChainToken.mint(address(s_crossChainToken), 1e18);
}

Expand Down
64 changes: 49 additions & 15 deletions chains/evm/contracts/tokens/BaseERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@ import {ITypeAndVersion} from "@chainlink/contracts/src/v0.8/shared/interfaces/I

import {ERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts@5.3.0/token/ERC20/extensions/IERC20Metadata.sol";
import {IERC165} from "@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol";

/// @notice A basic ERC20 compatible token contract with burn and minting roles.
/// @dev The total supply can be limited during deployment.
/// @dev If this contract is deployed with a pre-mint of 0, it is effectively useless as no mint functionality is
/// exposed.
contract BaseERC20 is IGetCCIPAdmin, ERC20, ITypeAndVersion, IERC165 {
function typeAndVersion() external pure virtual override returns (string memory) {
return "BaseERC20 2.0.0-dev";
}

error InvalidRecipient(address recipient);
error MaxSupplyExceeded(uint256 supplyAfterMint);
error CannotRenounceCCIPAdmin();
error MaxSupplyExceeded(uint256 supplyAfterMint, uint256 maxSupply);
error OnlyCCIPAdmin();
error PreMintAddressNotSet();
error PreMintSetWithZeroPrintMint(address preMintRecipient);

/// @notice Emitted when the CCIPAdmin role is transferred to a new address.
/// @param previousAdmin The address of the previous CCIPAdmin.
/// @param newAdmin The address of the new CCIPAdmin.
event CCIPAdminTransferred(address indexed previousAdmin, address indexed newAdmin);

/// @param name The name of the token
Expand Down Expand Up @@ -48,8 +54,7 @@ contract BaseERC20 is IGetCCIPAdmin, ERC20, ITypeAndVersion, IERC165 {
/// @dev The maximum supply of the token, 0 if unlimited
uint256 internal immutable i_maxSupply;

/// @dev the CCIPAdmin can be used to register with the CCIP token admin registry, but has no other special powers,
/// and can only be transferred by the owner.
/// @dev the CCIPAdmin can be used to register with the CCIP token admin registry, but has no other special powers.
address internal s_ccipAdmin;

constructor(
Expand All @@ -61,7 +66,6 @@ contract BaseERC20 is IGetCCIPAdmin, ERC20, ITypeAndVersion, IERC165 {
// Mint the initial supply to the preMintRecipient, saving gas by not calling if the mint amount is zero.
if (args.preMint != 0) {
if (args.preMintRecipient == address(0)) revert PreMintAddressNotSet();
if (args.maxSupply != 0 && args.preMint > args.maxSupply) revert MaxSupplyExceeded(args.preMint);

_mint(args.preMintRecipient, args.preMint);
} else if (args.preMintRecipient != address(0)) {
Expand All @@ -75,23 +79,27 @@ contract BaseERC20 is IGetCCIPAdmin, ERC20, ITypeAndVersion, IERC165 {
function supportsInterface(
bytes4 interfaceId
) public view virtual returns (bool) {
return interfaceId == type(IERC20).interfaceId || interfaceId == type(IGetCCIPAdmin).interfaceId;
return interfaceId == type(IERC20).interfaceId || interfaceId == type(IGetCCIPAdmin).interfaceId
|| interfaceId == type(IERC20Metadata).interfaceId || interfaceId == type(IERC165).interfaceId;
}

// ================================================================
// │ ERC20 │
// ================================================================

/// @dev Returns the number of decimals used in its user representation.
function decimals() public view virtual override returns (uint8) {
/// @notice Returns the number of decimals for this token.
/// @return _decimals The number of decimals for this token.
function decimals() public view virtual override returns (uint8 _decimals) {
return i_decimals;
}

/// @dev Returns the max supply of the token, 0 if unlimited.
function maxSupply() public view virtual returns (uint256) {
/// @notice Returns the max supply of the token, 0 if unlimited.
/// @return _maxSupply The max supply of the token, 0 if unlimited.
function maxSupply() public view virtual returns (uint256 _maxSupply) {
return i_maxSupply;
}

/// @inheritdoc ERC20
/// @dev Uses OZ ERC20 _approve to disallow approving for address(0).
/// @dev Disallows approving for address(this).
function _approve(
Expand All @@ -100,42 +108,68 @@ contract BaseERC20 is IGetCCIPAdmin, ERC20, ITypeAndVersion, IERC165 {
uint256 value,
bool emitEvent
) internal virtual override {
if (spender == address(this)) revert InvalidRecipient(spender);
if (spender == address(this)) revert ERC20InvalidSpender(spender);

super._approve(owner, spender, value, emitEvent);
}

/// @inheritdoc ERC20
/// @dev This check applies to transfer, minting, and burning.
/// @dev Disallows transferring/minting to address(this).
function _update(
address from,
address to,
uint256 value
) internal virtual override {
if (to == address(this)) revert InvalidRecipient(to);
if (to == address(this)) revert ERC20InvalidReceiver(to);

// Update first, then check the total supply.
super._update(from, to, value);

// If `from` is address(0), this is a mint, so we need to check the total supply against the max supply.
if (from == address(0)) {
_assertMaxSupply();
}
}

/// @notice Asserts that the total supply does not exceed the max supply. Reverts if it does.
function _assertMaxSupply() internal view virtual {
if (i_maxSupply != 0) {
uint256 supply = totalSupply();
if (supply > i_maxSupply) {
revert MaxSupplyExceeded(supply, i_maxSupply);
}
}
}

// ================================================================
// │ Roles │
// ================================================================

/// @notice Returns the current CCIPAdmin.
function getCCIPAdmin() external view virtual returns (address) {
/// @notice Gets the current CCIPAdmin.
/// @return ccipAdmin The address of the current CCIPAdmin.
function getCCIPAdmin() external view virtual returns (address ccipAdmin) {
return s_ccipAdmin;
}

/// @notice Transfers the CCIPAdmin role to a new address
/// @notice Transfers the CCIPAdmin role to a new address.
/// @param newAdmin The address of the new CCIPAdmin. Setting this to address(0) is not allowed.
/// @dev The BaseERC20 has no notion of ownership, so this function can be called by the CCIP admin. Tokens expanding
/// from this base contract can choose to restrict this function to other roles instead.
function setCCIPAdmin(
address newAdmin
) external virtual {
if (msg.sender != s_ccipAdmin) {
revert OnlyCCIPAdmin();
}

if (newAdmin == address(0)) revert CannotRenounceCCIPAdmin();

_setCCIPAdmin(newAdmin);
}

/// @dev Internal function to set the CCIPAdmin, emits an event with the previous and new admin.
/// @param newAdmin The address of the new CCIPAdmin.
function _setCCIPAdmin(
address newAdmin
) internal virtual {
Expand Down
Loading
Loading