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: added escrow contract #74

Merged
merged 5 commits into from
Oct 7, 2024
Merged
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
3 changes: 3 additions & 0 deletions e2e/interchaintestv8/e2esuite/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type DeployedContracts struct {
Ics26Router string `json:"ics26Router"`
Ics20Transfer string `json:"ics20Transfer"`
Erc20 string `json:"erc20"`
Escrow string `json:"escrow"`
}

// FundAddressChainB sends funds to the given address on Chain B.
Expand Down Expand Up @@ -133,6 +134,8 @@ func (s *TestSuite) GetEthContractsFromDeployOutput(stdout string) DeployedContr
s.Require().True(IsLowercase(embeddedContracts.Ics20Transfer))
s.Require().NotEmpty(embeddedContracts.Ics26Router)
s.Require().True(IsLowercase(embeddedContracts.Ics26Router))
s.Require().NotEmpty(embeddedContracts.Escrow)
s.Require().True(IsLowercase(embeddedContracts.Escrow))

return embeddedContracts
}
Expand Down
34 changes: 17 additions & 17 deletions e2e/interchaintestv8/ibc_eureka_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ type IbcEurekaTestSuite struct {

contractAddresses e2esuite.DeployedContracts

sp1Ics07Contract *sp1ics07tendermint.Contract
ics02Contract *ics02client.Contract
ics26Contract *ics26router.Contract
ics20Contract *ics20transfer.Contract
erc20Contract *erc20.Contract
sp1Ics07Contract *sp1ics07tendermint.Contract
ics02Contract *ics02client.Contract
ics26Contract *ics26router.Contract
ics20Contract *ics20transfer.Contract
erc20Contract *erc20.Contract
escrowContractAddr ethcommon.Address

simdClientID string
ethClientID string
Expand Down Expand Up @@ -192,6 +193,7 @@ func (s *IbcEurekaTestSuite) SetupSuite(ctx context.Context) {
s.Require().NoError(err)
s.erc20Contract, err = erc20.NewContract(ethcommon.HexToAddress(s.contractAddresses.Erc20), ethClient)
s.Require().NoError(err)
s.escrowContractAddr = ethcommon.HexToAddress(s.contractAddresses.Escrow)
}))

s.T().Cleanup(func() {
Expand Down Expand Up @@ -407,9 +409,9 @@ func (s *IbcEurekaTestSuite) TestICS20TransferERC20TokenfromEthereumToCosmosAndB
s.Require().Equal(testvalues.InitialBalance-testvalues.TransferAmount, userBalance.Int64())

// ICS20 contract balance on Ethereum
ics20TransferBalance, err := s.erc20Contract.BalanceOf(nil, ics20Address)
escrowBalance, err := s.erc20Contract.BalanceOf(nil, s.escrowContractAddr)
s.Require().NoError(err)
s.Require().Equal(transferAmount, ics20TransferBalance)
s.Require().Equal(transferAmount, escrowBalance)
}))
}))

Expand Down Expand Up @@ -497,9 +499,9 @@ func (s *IbcEurekaTestSuite) TestICS20TransferERC20TokenfromEthereumToCosmosAndB
s.Require().Equal(testvalues.InitialBalance-testvalues.TransferAmount, userBalance.Int64())

// ICS20 contract balance on Ethereum
ics20Bal, err := s.erc20Contract.BalanceOf(nil, ics20Address)
escrowBalance, err := s.erc20Contract.BalanceOf(nil, s.escrowContractAddr)
s.Require().NoError(err)
s.Require().Equal(testvalues.TransferAmount, ics20Bal.Int64())
s.Require().Equal(testvalues.TransferAmount, escrowBalance.Int64())
}))
}))

Expand Down Expand Up @@ -619,9 +621,9 @@ func (s *IbcEurekaTestSuite) TestICS20TransferERC20TokenfromEthereumToCosmosAndB
s.Require().NoError(err)
s.Require().Equal(testvalues.InitialBalance, userBalance.Int64())

ics20TransferBalance, err := s.erc20Contract.BalanceOf(nil, ics20Address)
escrowBalance, err := s.erc20Contract.BalanceOf(nil, s.escrowContractAddr)
s.Require().NoError(err)
s.Require().Equal(int64(0), ics20TransferBalance.Int64())
s.Require().Equal(int64(0), escrowBalance.Int64())
}))
}))

Expand Down Expand Up @@ -1044,10 +1046,9 @@ func (s *IbcEurekaTestSuite) TestICS20TransferTimeoutFromEthereumToCosmosChain()
s.Require().Equal(testvalues.InitialBalance-testvalues.TransferAmount, userBalance.Int64())

// ICS20 contract balance on Ethereum
ics20Address := ethcommon.HexToAddress(s.contractAddresses.Ics20Transfer)
ics20Bal, err := s.erc20Contract.BalanceOf(nil, ics20Address)
escrowBalance, err := s.erc20Contract.BalanceOf(nil, s.escrowContractAddr)
s.Require().NoError(err)
s.Require().Equal(transferAmount, ics20Bal)
s.Require().Equal(transferAmount, escrowBalance)
}))
}))

Expand Down Expand Up @@ -1094,10 +1095,9 @@ func (s *IbcEurekaTestSuite) TestICS20TransferTimeoutFromEthereumToCosmosChain()
s.Require().Equal(testvalues.InitialBalance, userBalance.Int64())

// ICS20 contract balance on Ethereum
ics20Address := ethcommon.HexToAddress(s.contractAddresses.Ics20Transfer)
ics20Bal, err := s.erc20Contract.BalanceOf(nil, ics20Address)
escrowBalance, err := s.erc20Contract.BalanceOf(nil, s.escrowContractAddr)
s.Require().NoError(err)
s.Require().Equal(int64(0), ics20Bal.Int64())
s.Require().Equal(int64(0), escrowBalance.Int64())
}))
}))
}
1 change: 1 addition & 0 deletions scripts/E2ETestDeploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ contract E2ETestDeploy is Script {
json.serialize("ics02Client", Strings.toHexString(address(ics02Client)));
json.serialize("ics26Router", Strings.toHexString(address(ics26Router)));
json.serialize("ics20Transfer", Strings.toHexString(address(ics20Transfer)));
json.serialize("escrow", Strings.toHexString(ics20Transfer.escrow()));
string memory finalJson = json.serialize("erc20", Strings.toHexString(address(erc20)));

return finalJson;
Expand Down
2 changes: 2 additions & 0 deletions scripts/MockE2ETestDeploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ contract MockE2ETestDeploy is Script {
// Mint some tokens
(address addr, bool ok) = ICS20Lib.hexStringToAddress(E2E_FAUCET);
require(ok, "invalid address");

erc20.mint(addr, 1_000_000_000_000_000_000);

vm.stopBroadcast();
Expand All @@ -77,6 +78,7 @@ contract MockE2ETestDeploy is Script {
json.serialize("ics02Client", Strings.toHexString(address(ics02Client)));
json.serialize("ics26Router", Strings.toHexString(address(ics26Router)));
json.serialize("ics20Transfer", Strings.toHexString(address(ics20Transfer)));
json.serialize("escrow", Strings.toHexString(ics20Transfer.escrow()));
string memory finalJson = json.serialize("erc20", Strings.toHexString(address(erc20)));

return finalJson;
Expand Down
29 changes: 20 additions & 9 deletions src/ICS20Transfer.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { IEscrow } from "./interfaces/IEscrow.sol";
import { IIBCApp } from "./interfaces/IIBCApp.sol";
import { IICS20Errors } from "./errors/IICS20Errors.sol";
import { ICS20Lib } from "./utils/ICS20Lib.sol";
Expand All @@ -13,6 +14,7 @@ import { IICS26Router } from "./interfaces/IICS26Router.sol";
import { IICS26RouterMsgs } from "./msgs/IICS26RouterMsgs.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { IBCERC20 } from "./utils/IBCERC20.sol";
import { Escrow } from "./utils/Escrow.sol";

using SafeERC20 for IERC20;

Expand All @@ -22,11 +24,20 @@ using SafeERC20 for IERC20;
* - Related to escrow ^: invariant checking (where to implement that?)
*/
contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, ReentrancyGuard {
/// @notice The escrow contract address
IEscrow private immutable ESCROW;
/// @notice Mapping of non-native denoms to their respective IBCERC20 contracts created here
mapping(string denom => IBCERC20 ibcERC20Contract) private _foreignDenomContracts;
mapping(string denom => IBCERC20 ibcERC20Contract) private _ibcDenomContracts;

/// @param owner_ The owner of the contract
constructor(address owner_) Ownable(owner_) { }
constructor(address owner_) Ownable(owner_) {
ESCROW = new Escrow(address(this));
}

/// @inheritdoc IICS20Transfer
function escrow() external view override returns (address) {
return address(ESCROW);
}

/// @inheritdoc IICS20Transfer
function sendTransfer(SendTransferMsg calldata msg_) external override returns (uint32) {
Expand Down Expand Up @@ -84,7 +95,7 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr
(address erc20Address, bool originatorChainIsSource) = getSendERC20AddressAndSource(msg_.packet, packetData);

// transfer the tokens to us (requires the allowance to be set)
_transferFrom(sender, address(this), erc20Address, packetData.amount);
_transferFrom(sender, address(ESCROW), erc20Address, packetData.amount);

if (!originatorChainIsSource) {
// receiver chain is source: burn the vouchers
Expand Down Expand Up @@ -124,7 +135,7 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr
}

// transfer the tokens to the receiver
IERC20(erc20Address).safeTransfer(receiver, packetData.amount);
ESCROW.send(IERC20(erc20Address), receiver, packetData.amount);

// Note the event don't take into account the conversion
emit ICS20ReceiveTransfer(packetData, erc20Address);
Expand Down Expand Up @@ -159,7 +170,7 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr
/// @param erc20Address The address of the ERC20 contract
function _refundTokens(ICS20Lib.PacketDataJSON memory packetData, address erc20Address) private {
address refundee = ICS20Lib.mustHexStringToAddress(packetData.sender);
IERC20(erc20Address).safeTransfer(refundee, packetData.amount);
ESCROW.send(IERC20(erc20Address), refundee, packetData.amount);
}

/// @notice Transfer tokens from sender to receiver
Expand Down Expand Up @@ -209,7 +220,7 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr
} else {
// receiving chain is source of the token, so we've received and mapped this token before
string memory ibcDenom = ICS20Lib.toIBCDenom(packetData.denom);
erc20Address = address(_foreignDenomContracts[ibcDenom]);
erc20Address = address(_ibcDenomContracts[ibcDenom]);
if (erc20Address == address(0)) {
revert ICS20DenomNotFound(packetData.denom);
}
Expand Down Expand Up @@ -259,11 +270,11 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr
function findOrCreateERC20Address(string memory fullDenomPath, string memory base) internal returns (address) {
// check if denom already has a foreign registered contract
string memory ibcDenom = ICS20Lib.toIBCDenom(fullDenomPath);
address erc20Contract = address(_foreignDenomContracts[ibcDenom]);
address erc20Contract = address(_ibcDenomContracts[ibcDenom]);
if (erc20Contract == address(0)) {
// nothing exists, so we create new erc20 contract and register it in the mapping
IBCERC20 ibcERC20 = new IBCERC20(IICS20Transfer(address(this)), ibcDenom, base, fullDenomPath);
_foreignDenomContracts[ibcDenom] = ibcERC20;
IBCERC20 ibcERC20 = new IBCERC20(this, ESCROW, ibcDenom, base, fullDenomPath);
_ibcDenomContracts[ibcDenom] = ibcERC20;
erc20Contract = address(ibcERC20);
}

Expand Down
14 changes: 14 additions & 0 deletions src/interfaces/IEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @title Escrow Contract Interface
/// @notice This interface is implemented by the Escrow contract
interface IEscrow {
/// @notice Send tokens to the specified address
/// @param token The token to send
/// @param to The address to send the tokens to
/// @param amount The amount of tokens to send
function send(IERC20 token, address to, uint256 amount) external;
}
4 changes: 2 additions & 2 deletions src/interfaces/IIBCERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ pragma solidity >=0.8.25;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IIBCERC20 is IERC20 {
/// @notice Mint new tokens to the ICS20Transfer contract
/// @notice Mint new tokens to the Escrow contract
/// @param amount Amount of tokens to mint
function mint(uint256 amount) external;

/// @notice Burn tokens from the ICS20Transfer contract
/// @notice Burn tokens from the Escrow contract
/// @param amount Amount of tokens to burn
function burn(uint256 amount) external;

Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/IICS20Transfer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ interface IICS20Transfer is IICS20TransferMsgs {
/// @param msg The message for sending a transfer
/// @return sequence The sequence number of the packet created
function sendTransfer(SendTransferMsg calldata msg) external returns (uint32 sequence);

/// @notice Retrieve the escrow contract address
/// @return The escrow contract address
function escrow() external view returns (address);
}
22 changes: 22 additions & 0 deletions src/utils/Escrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IEscrow } from "../interfaces/IEscrow.sol";

using SafeERC20 for IERC20;

/// @title Escrow Contract
/// @notice This contract is used to escrow the funds for the ICS20 contract
contract Escrow is Ownable, IEscrow {
/// @param owner_ The owner of the contract
/// @dev Owner is to be the ICS20Transfer contract
constructor(address owner_) Ownable(owner_) { }

/// @inheritdoc IEscrow
function send(IERC20 token, address to, uint256 amount) external override onlyOwner {
token.safeTransfer(to, amount);
}
}
9 changes: 7 additions & 2 deletions src/utils/IBCERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IICS20Transfer } from "../interfaces/IICS20Transfer.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IIBCERC20 } from "../interfaces/IIBCERC20.sol";
import { IEscrow } from "../interfaces/IEscrow.sol";

contract IBCERC20 is IIBCERC20, ERC20, Ownable {
/// @notice The full IBC denom path for this token
string private _fullDenomPath;
/// @notice The escrow contract address
IEscrow private immutable ESCROW;

constructor(
IICS20Transfer owner_,
IEscrow escrow_,
string memory ibcDenom_,
string memory baseDenom_,
string memory fullDenomPath_
Expand All @@ -20,6 +24,7 @@ contract IBCERC20 is IIBCERC20, ERC20, Ownable {
Ownable(address(owner_))
{
_fullDenomPath = fullDenomPath_;
ESCROW = escrow_;
}

/// @inheritdoc IIBCERC20
Expand All @@ -29,11 +34,11 @@ contract IBCERC20 is IIBCERC20, ERC20, Ownable {

/// @inheritdoc IIBCERC20
function mint(uint256 amount) external onlyOwner {
_mint(owner(), amount);
_mint(address(ESCROW), amount);
}

/// @inheritdoc IIBCERC20
function burn(uint256 amount) external onlyOwner {
_burn(owner(), amount);
_burn(address(ESCROW), amount);
}
}
Loading
Loading