From cd3e1de27b8703c31e1d19b5c4517e20a340a3be Mon Sep 17 00:00:00 2001 From: Web3 Philosopher Date: Thu, 12 Sep 2024 19:16:23 +0100 Subject: [PATCH] Remove ERC20 support from TokenGateway (#306) --- evm/src/modules/TokenGateway.sol | 210 +----------------- evm/test/TokenGatewayForkTest.sol | 2 - evm/test/TokenGatewayTest.sol | 188 +--------------- .../ismp/pallets/token-governor/src/types.rs | 4 - 4 files changed, 11 insertions(+), 393 deletions(-) diff --git a/evm/src/modules/TokenGateway.sol b/evm/src/modules/TokenGateway.sol index 91af112b3..c395c5747 100644 --- a/evm/src/modules/TokenGateway.sol +++ b/evm/src/modules/TokenGateway.sol @@ -30,32 +30,28 @@ import {ICallDispatcher, CallDispatcherParams} from "./CallDispatcher.sol"; struct TeleportParams { // amount to be sent uint256 amount; - // Maximum amount to pay for liquidity fees - uint256 maxFee; // Relayer fee uint256 relayerFee; - // The token identifier + // The token identifier to send bytes32 assetId; - // Redeem Erc20 on the destination? + // Redeem ERC20 on the destination? bool redeem; // recipient address bytes32 to; // recipient state machine bytes dest; - // timeout in seconds + // request timeout in seconds uint64 timeout; - // destination contract call data - bytes data; // Amount of native token to pay for dispatching the request // if 0 will use the `IIsmpHost.feeToken` uint256 nativeCost; + // destination contract call data + bytes data; } struct Body { // Amount of the asset to be sent uint256 amount; - // Maximum amount to pay for liquidity fees - uint256 maxFee; // The asset identifier bytes32 assetId; // Flag to redeem the erc20 asset on the destination @@ -69,8 +65,6 @@ struct Body { struct BodyWithCall { // Amount of the asset to be sent uint256 amount; - // Maximum amount to pay for liquidity fees - uint256 maxFee; // The asset identifier bytes32 assetId; // Flag to redeem the erc20 asset on the destination @@ -149,7 +143,7 @@ struct DeregsiterAsset { } // Abi-encoded size of Body struct -uint256 constant BODY_BYTES_SIZE = 193; +uint256 constant BODY_BYTES_SIZE = 161; // Params for the TokenGateway contract struct TokenGatewayParams { @@ -159,20 +153,13 @@ struct TokenGatewayParams { address dispatcher; } -struct LiquidityBid { - // Bidder in question - address bidder; - // Proposed fee - uint256 fee; -} - /** * @title The TokenGateway. * @author Polytope Labs (hello@polytope.technology) * * @notice Allows users send either ERC20 or ERC6160 tokens using Hyperbridge as a message-passing layer. * - * @dev If ERC20 tokens are sent then fillers bid to provide the ERC20 token on the destination chain. + * @dev ERC20 tokens are custodied in exchange for ERC6160 tokens to be minted on the destination chain, * Otherwise if ERC6160 tokens are sent, then it simply performs a burn-and-mint. */ contract TokenGateway is BaseIsmpModule { @@ -191,44 +178,9 @@ contract TokenGateway is BaseIsmpModule { // mapping of token identifier to erc20 contracts mapping(bytes32 => address) private _erc20s; - // mapping of a request commitment to a corresponding bid - mapping(bytes32 => LiquidityBid) private _bids; - // mapping of keccak256(source chain) to the token gateway contract address mapping(bytes32 => address) private _instances; - // A filler has just placed a bid to fulfil some request - event BidPlaced( - // The associated request commitment - bytes32 commitment, - // The liquidity fee for the bid - uint256 bid, - // The assetId for the bid - bytes32 indexed assetId, - // The bidder's address - address indexed bidder - ); - - // The request associated with a bid has timed out and the bid refunded - event BidRefunded( - // The associated request commitment - bytes32 commitment, - // The assetId for the bid - bytes32 indexed assetId, - // The bidder's address - address indexed bidder - ); - - // Filler fulfilled some liquidity request - event RequestFulfilled( - // The amount that was provided to the user - uint256 amount, - // The bidder's address - address indexed bidder, - // The provided assetId - bytes32 indexed assetId - ); - // User has received some assets event AssetReceived( // The amount that was provided to the user @@ -321,21 +273,6 @@ contract TokenGateway is BaseIsmpModule { // @dev Action is unauthorized error UnauthorizedAction(); - // @dev Provided request has timed out - error RequestTimedOut(); - - // @dev Provided request has not timed out - error RequestNotTimedOut(); - - // @dev Provided bid cannot usurp the existing bid - error BidTooHigh(); - - // @dev Unfortunately no one has bid to fulfil this request - error NoExistingBid(); - - // @dev Provided request already fulfilled - error RequestAlreadyFulfilled(); - // @dev Unexpected zero address error ZeroAddress(); @@ -411,13 +348,6 @@ contract TokenGateway is BaseIsmpModule { return _erc6160s[assetId]; } - /** - * @dev Fetch the bid for a given request commitment - */ - function bid(bytes32 commitment) public view returns (LiquidityBid memory) { - return _bids[commitment]; - } - /** * @dev Fetch the TokenGateway instance for a destination. */ @@ -467,7 +397,6 @@ contract TokenGateway is BaseIsmpModule { from: addressToBytes32(msg.sender), to: teleportParams.to, amount: teleportParams.amount, - maxFee: teleportParams.maxFee, assetId: teleportParams.assetId, redeem: teleportParams.redeem, data: teleportParams.data @@ -477,7 +406,6 @@ contract TokenGateway is BaseIsmpModule { Body({ from: addressToBytes32(msg.sender), to: teleportParams.to, - maxFee: teleportParams.maxFee, amount: teleportParams.amount, assetId: teleportParams.assetId, redeem: teleportParams.redeem @@ -514,111 +442,6 @@ contract TokenGateway is BaseIsmpModule { }); } - /** - * @dev Bid to fulfil an incoming asset. This will displace any pre-existing bids - * if the liquidity fee is lower than said bid. This effectively creates a - * race to the bottom for fees. - * - * @notice The request must not have expired, and must not have already been fulfilled. - */ - function bid(PostRequest calldata request, uint256 fee) public authenticate(request) { - // Not sure why anyone would do this - if (!request.dest.equals(IIsmpHost(_params.host).host())) revert UnauthorizedAction(); - // cannot bid on timed-out requests - if (block.timestamp > request.timeout()) revert RequestTimedOut(); - - bytes32 commitment = request.hash(); - // cannot bid on fulfilled requests - if (IIsmpHost(_params.host).requestReceipts(commitment) != address(0)) revert RequestAlreadyFulfilled(); - - Body memory body; - if (request.body.length > BODY_BYTES_SIZE) { - BodyWithCall memory bodyWithCall = abi.decode(request.body[1:], (BodyWithCall)); - body = Body({ - amount: bodyWithCall.amount, - maxFee: bodyWithCall.maxFee, - assetId: bodyWithCall.assetId, - redeem: bodyWithCall.redeem, - from: bodyWithCall.from, - to: bodyWithCall.to - }); - } else { - body = abi.decode(request.body[1:], (Body)); - } - - if (body.redeem) revert UnauthorizedAction(); - - address erc20Address = _erc20s[body.assetId]; - if (erc20Address == address(0)) revert UnknownAsset(); - - LiquidityBid memory liquidityBid = _bids[commitment]; - - // no existing bids - if (liquidityBid.bidder == address(0)) { - if (fee > body.maxFee) revert BidTooHigh(); - - // transfer from bidder to this - SafeERC20.safeTransferFrom(IERC20(erc20Address), msg.sender, address(this), body.amount - fee); - } else { - if (fee >= liquidityBid.fee) revert BidTooHigh(); - // refund previous bidder - SafeERC20.safeTransfer(IERC20(erc20Address), liquidityBid.bidder, body.amount - liquidityBid.fee); - - // transfer from new bidder to this - SafeERC20.safeTransferFrom(IERC20(erc20Address), msg.sender, address(this), body.amount - fee); - } - - _bids[commitment] = LiquidityBid({bidder: msg.sender, fee: fee}); - - // emit event - emit BidPlaced({commitment: commitment, assetId: body.assetId, bid: fee, bidder: msg.sender}); - } - - /** - * @dev This allows the bidder to refund their bids in the event that the request timed-out before - * the bid could be fulfilled. - */ - function refundBid(PostRequest calldata request) public authenticate(request) { - // Not sure why anyone would do this - if (!request.dest.equals(IIsmpHost(_params.host).host())) revert UnauthorizedAction(); - // Cannot refund bids on requests which have not timed out, sorry. - if (request.timeout() > block.timestamp) revert RequestNotTimedOut(); - - bytes32 commitment = request.hash(); - // cannot refund bids for fulfilled requests - if (IIsmpHost(_params.host).requestReceipts(commitment) != address(0)) revert RequestAlreadyFulfilled(); - - LiquidityBid memory liquidityBid = _bids[commitment]; - if (liquidityBid.bidder == address(0)) revert NoExistingBid(); - - Body memory body; - if (request.body.length > BODY_BYTES_SIZE) { - BodyWithCall memory bodyWithCall = abi.decode(request.body[1:], (BodyWithCall)); - body = Body({ - amount: bodyWithCall.amount, - maxFee: bodyWithCall.maxFee, - assetId: bodyWithCall.assetId, - redeem: bodyWithCall.redeem, - from: bodyWithCall.from, - to: bodyWithCall.to - }); - } else { - body = abi.decode(request.body[1:], (Body)); - } - - address erc20Address = _erc20s[body.assetId]; - - // can only happen if someone bids on an asset right before it was deregistered. - // In this case, the asset will need to be re-registered - if (erc20Address == address(0)) revert UnknownAsset(); - - delete _bids[commitment]; - - SafeERC20.safeTransfer(IERC20(erc20Address), liquidityBid.bidder, body.amount - liquidityBid.fee); - - emit BidRefunded({commitment: commitment, assetId: body.assetId, bidder: liquidityBid.bidder}); - } - /** * @dev Entry point for all cross-chain messages. */ @@ -655,7 +478,6 @@ contract TokenGateway is BaseIsmpModule { body = Body({ amount: bodyWithCall.amount, assetId: bodyWithCall.assetId, - maxFee: bodyWithCall.maxFee, redeem: bodyWithCall.redeem, from: bodyWithCall.from, to: bodyWithCall.to @@ -687,7 +509,7 @@ contract TokenGateway is BaseIsmpModule { ) internal authenticate(incoming.request) { Body memory body = abi.decode(incoming.request.body[1:], (Body)); bytes32 commitment = incoming.request.hash(); - handleIncomingAsset(body, commitment); + handleIncomingAsset(body); emit AssetReceived({ commitment: commitment, @@ -710,13 +532,11 @@ contract TokenGateway is BaseIsmpModule { handleIncomingAsset( Body({ amount: body.amount, - maxFee: body.maxFee, assetId: body.assetId, redeem: body.redeem, from: body.from, to: body.to - }), - commitment + }) ); ICallDispatcher(_params.dispatcher).dispatch(body.data); @@ -733,23 +553,13 @@ contract TokenGateway is BaseIsmpModule { /** * @dev Executes the asset disbursement for the provided request */ - function handleIncomingAsset(Body memory body, bytes32 commitment) internal { + function handleIncomingAsset(Body memory body) internal { address _erc20 = _erc20s[body.assetId]; address _erc6160 = _erc6160s[body.assetId]; if (_erc20 != address(0) && body.redeem) { // a relayer/user is redeeming the native asset SafeERC20.safeTransfer(IERC20(_erc20), bytes32ToAddress(body.to), body.amount); - } else if (_erc20 != address(0) && _erc6160 != address(0) && !body.redeem) { - // user is swapping, fetch the bid - LiquidityBid memory liquidityBid = _bids[commitment]; - if (liquidityBid.bidder == address(0)) revert NoExistingBid(); - - uint256 value = body.amount - liquidityBid.fee; - SafeERC20.safeTransfer(IERC20(_erc20), bytes32ToAddress(body.to), value); - // hand the bidder the receipt so they can redeem the asset on the source chain - IERC6160Ext20(_erc6160).mint(liquidityBid.bidder, body.amount); - emit RequestFulfilled({bidder: liquidityBid.bidder, amount: value, assetId: body.assetId}); } else if (_erc6160 != address(0)) { IERC6160Ext20(_erc6160).mint(bytes32ToAddress(body.to), body.amount); } else { diff --git a/evm/test/TokenGatewayForkTest.sol b/evm/test/TokenGatewayForkTest.sol index 65467d96d..95321b537 100644 --- a/evm/test/TokenGatewayForkTest.sol +++ b/evm/test/TokenGatewayForkTest.sol @@ -51,7 +51,6 @@ contract TeleportForkTest is MainnetForkBaseTest { gateway.teleport{value: _amountInMax}( TeleportParams({ - maxFee: 1 * 1e6, amount: 1_000 * 1e6, // $1000 redeem: false, dest: StateMachine.evm(97), @@ -87,7 +86,6 @@ contract TeleportForkTest is MainnetForkBaseTest { vm.startPrank(whaleAccount); gateway.teleport{value: total}( TeleportParams({ - maxFee: 0, amount: teleportAmount, // $1000 redeem: false, dest: StateMachine.evm(97), diff --git a/evm/test/TokenGatewayTest.sol b/evm/test/TokenGatewayTest.sol index 45c875e0f..d563a1939 100644 --- a/evm/test/TokenGatewayTest.sol +++ b/evm/test/TokenGatewayTest.sol @@ -41,7 +41,6 @@ contract TokenGatewayTest is BaseTest { TeleportParams({ amount: 1000 * 1e18, // $1000 redeem: false, - maxFee: 1 * 1e18, dest: StateMachine.evm(97), relayerFee: 9 * 1e17, // $0.9 timeout: 0, @@ -58,7 +57,7 @@ contract TokenGatewayTest is BaseTest { function testCanTeleportAssetsWithCall() public { // relayer fee + per-byte fee - uint256 messagingFee = (9 * 1e17) + (353 * host.perByteFee()); + uint256 messagingFee = (9 * 1e17) + (321 * host.perByteFee()); uint256 totalFee = 1_000 * 1e18 + messagingFee; feeToken.mint(address(this), totalFee); @@ -71,7 +70,6 @@ contract TokenGatewayTest is BaseTest { TeleportParams({ amount: 1000 * 1e18, // $1000 redeem: false, - maxFee: 1 * 1e18, dest: StateMachine.evm(97), relayerFee: 9 * 1e17, // $0.9 timeout: 0, @@ -94,7 +92,6 @@ contract TokenGatewayTest is BaseTest { TeleportParams({ amount: 1000 * 1e18, // $1000 redeem: false, - maxFee: 1 * 1e18, dest: StateMachine.evm(97), relayerFee: 9 * 1e17, // $0.9 timeout: 0, @@ -113,7 +110,6 @@ contract TokenGatewayTest is BaseTest { assetId: keccak256("USD.h"), to: addressToBytes32(address(this)), redeem: false, - maxFee: 1 * 1e18, amount: 1_000 * 1e18, from: addressToBytes32(address(this)) }); @@ -150,7 +146,6 @@ contract TokenGatewayTest is BaseTest { assetId: keccak256("USD.h"), to: addressToBytes32(address(miniStaking)), redeem: false, - maxFee: 1 * 1e18, amount: 1_000 * 1e18, from: addressToBytes32(address(this)), data: abi.encode(calls) @@ -182,7 +177,6 @@ contract TokenGatewayTest is BaseTest { assetId: keccak256("USD.h"), to: addressToBytes32(address(this)), redeem: false, - maxFee: 1 * 1e18, amount: 1_000 * 1e18, from: addressToBytes32(address(this)) }); @@ -207,7 +201,6 @@ contract TokenGatewayTest is BaseTest { to: addressToBytes32(address(miniStaking)), redeem: false, amount: 1_000 * 1e18, - maxFee: 1 * 1e18, from: addressToBytes32(address(this)), data: stakeCalldata }); @@ -357,7 +350,6 @@ contract TokenGatewayTest is BaseTest { to: addressToBytes32(address(this)), redeem: false, amount: 1_000 * 1e18, - maxFee: 1 * 1e18, from: addressToBytes32(address(this)) }); vm.expectRevert(TokenGateway.UnauthorizedAction.selector); @@ -383,7 +375,6 @@ contract TokenGatewayTest is BaseTest { to: addressToBytes32(address(this)), redeem: false, amount: 1_000 * 1e18, - maxFee: 1 * 1e18, from: addressToBytes32(address(this)) }); vm.startPrank(address(host)); @@ -405,182 +396,6 @@ contract TokenGatewayTest is BaseTest { ); } - function testHandleIncomingAssetWithSwap() public { - // Adding new Asset to the gateway - AssetMetadata memory asset = AssetMetadata({ - erc20: address(hyperInu), - erc6160: address(hyperInu_h), - name: "HyperInu", - symbol: "HINU.h", - beneficiary: address(0), - initialSupply: 0 - }); - - bytes memory hyperbridge = StateMachine.kusama(2000); - - // relayer fee + per-byte fee - uint256 messagingFee = (9 * 1e17) + (BODY_BYTES_SIZE * host.perByteFee()); - feeToken.mint(address(this), 1_000 * 1e18 + messagingFee); - - vm.prank(address(host)); - gateway.onAccept( - IncomingPostRequest({ - request: PostRequest({ - to: abi.encodePacked(address(0)), - from: abi.encodePacked(address(gateway)), - dest: new bytes(0), - body: bytes.concat(hex"02", abi.encode(asset)), - nonce: 0, - source: hyperbridge, - timeoutTimestamp: 0 - }), - relayer: address(0) - }) - ); - - address user_vault = address(1); - address relayer_address = address(tx.origin); - - hyperInu.mint(relayer_address, 1_000 * 1e18); - hyperInu.superApprove(relayer_address, address(gateway)); - uint256 liquidityFee = 3 * 1e18; // 0.3% of the total amount (997000000000000000000) - - Body memory body = Body({ - assetId: keccak256("HINU.h"), - to: addressToBytes32(user_vault), - redeem: false, - amount: 1_000 * 1e18, - maxFee: liquidityFee, - from: addressToBytes32(address(this)) - }); - - uint256 relayerBalanceBefore = hyperInu_h.balanceOf(relayer_address); - - PostRequest memory request = PostRequest({ - to: abi.encodePacked(address(0)), - from: abi.encodePacked(address(gateway)), - dest: host.host(), - body: bytes.concat(hex"00", abi.encode(body)), - nonce: 0, - source: new bytes(0), - timeoutTimestamp: 0 - }); - - vm.prank(address(relayer_address)); - gateway.bid(request, liquidityFee); - - // hitting the gateway with the incoming asset - vm.prank(address(host)); - gateway.onAccept(IncomingPostRequest({request: request, relayer: relayer_address})); - - uint256 relayerBalanceAfter = hyperInu_h.balanceOf(relayer_address); - - assert(hyperInu.balanceOf(user_vault) == 1_000 * 1e18 - liquidityFee); // user should have the ERC20 token - fee - assert((relayerBalanceAfter - relayerBalanceBefore) == 1_000 * 1e18); // relayer should have the ERC6160 token - } - - function testBidInvariants() public { - // create the asset - testHandleIncomingAssetWithSwap(); - - Body memory body = Body({ - assetId: keccak256("HINU.h"), - to: addressToBytes32(address(this)), - redeem: false, - maxFee: 1e18, - amount: 1_000 * 1e18, - from: addressToBytes32(address(this)) - }); - - PostRequest memory request = PostRequest({ - to: abi.encodePacked(address(0)), - from: abi.encodePacked(address(0)), - dest: new bytes(0), - body: bytes.concat(hex"00", abi.encode(body)), - nonce: 0, - source: new bytes(0), - timeoutTimestamp: 0 - }); - - vm.expectRevert(TokenGateway.UnauthorizedAction.selector); - gateway.bid(request, 1e18); - - request.from = abi.encodePacked(address(gateway)); - vm.expectRevert(TokenGateway.UnauthorizedAction.selector); - gateway.bid(request, 1e18); - - request.dest = host.host(); - vm.expectRevert(TokenGateway.BidTooHigh.selector); - gateway.bid(request, 10e18); - - // ok bid for real this time - hyperInu.mint(address(this), 1_000 * 1e18); - hyperInu.superApprove(address(this), address(gateway)); - gateway.bid(request, 1e18); - - // can't usurp bids with the same price - vm.expectRevert(TokenGateway.BidTooHigh.selector); - gateway.bid(request, 1e18); - - // usurp bid with less - hyperInu.mint(address(11111), 1_000 * 1e18); - hyperInu.superApprove(address(11111), address(gateway)); - vm.prank(address(11111)); - gateway.bid(request, 9e17); - - // bid refunded - assert(hyperInu.balanceOf(address(this)) == 1_000 * 1e18); - assert(hyperInu.balanceOf(address(11111)) == 9e17); - } - - function testRefundBid() public { - // create the asset - testHandleIncomingAssetWithSwap(); - - Body memory body = Body({ - assetId: keccak256("HINU.h"), - to: addressToBytes32(address(this)), - redeem: false, - maxFee: 1e18, - amount: 1_000 * 1e18, - from: addressToBytes32(address(this)) - }); - - PostRequest memory request = PostRequest({ - to: abi.encodePacked(address(gateway)), - from: abi.encodePacked(address(gateway)), - dest: host.host(), - body: bytes.concat(hex"00", abi.encode(body)), - nonce: 0, - source: new bytes(0), - timeoutTimestamp: 1_000 - }); - - hyperInu.mint(address(this), 1_000 * 1e18); - hyperInu.superApprove(address(this), address(gateway)); - gateway.bid(request, 1e18); - assert(hyperInu.balanceOf(address(this)) == 1e18); - - vm.expectRevert(TokenGateway.RequestNotTimedOut.selector); - gateway.refundBid(request); - - // advance the time so that refunds can pass - vm.warp(1_001); - gateway.refundBid(request); - assert(hyperInu.balanceOf(address(this)) == 1_000 * 1e18); - - // bid again and dispatch the request - vm.warp(1); - gateway.bid(request, 1e18); - vm.prank(address(handler)); - host.dispatchIncoming(request, address(1111111)); - - // then try to ask for a refund - vm.warp(1_001); - vm.expectRevert(TokenGateway.RequestAlreadyFulfilled.selector); - gateway.refundBid(request); - } - function testCanModifyProtocolParams() public { TokenGatewayParams memory params = gateway.params(); @@ -669,7 +484,6 @@ contract TokenGatewayTest is BaseTest { assetId: keccak256("USD.h"), to: addressToBytes32(address(this)), redeem: false, - maxFee: 0, amount: 1_000 * 1e18, from: addressToBytes32(address(this)) }); diff --git a/modules/ismp/pallets/token-governor/src/types.rs b/modules/ismp/pallets/token-governor/src/types.rs index 81c951656..daad26a9c 100644 --- a/modules/ismp/pallets/token-governor/src/types.rs +++ b/modules/ismp/pallets/token-governor/src/types.rs @@ -83,10 +83,6 @@ pub struct Params { /// Struct for updating the protocol parameters for the TokenGovernor #[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq)] pub struct ParamsUpdate { - /// The address of the token gateway contract across all chains - pub token_gateway_address: Option, - /// The address of the token registrar contract across all chains - pub token_registrar_address: Option, /// The asset registration fee in native tokens, collected by the treasury pub registration_fee: Option, }