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

Frontend: support subsidized transfers #2031

Merged
merged 10 commits into from
Jul 12, 2023
34 changes: 34 additions & 0 deletions beamer/tests/contracts/test_fee_sub.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,40 @@ def test_withdrawn_from_request_manager(fee_sub, token, request_manager):
assert fee_sub.senders(request_id) == ADDRESS_ZERO


def test_amount_can_be_subsidized(fee_sub, token, request_manager):
transfer_amount = 95_000_000
token_address = token.address
target_chain_id = ape.chain.chain_id

# token subsidy is not activated for the token address
fee_sub.setMinimumAmount(token.address, 0)
can_be_subsidized = fee_sub.tokenAmountCanBeSubsidized(
target_chain_id, token_address, transfer_amount
)
assert can_be_subsidized is False

# transfer_amount is lower than the defined minimumAmount threshold
fee_sub.setMinimumAmount(token.address, 95_000_001)
can_be_subsidized = fee_sub.tokenAmountCanBeSubsidized(
target_chain_id, token_address, transfer_amount
)
assert can_be_subsidized is False

# fee amount is higher than the contracts token balance
fee_sub.setMinimumAmount(token.address, 95_000_000)
request_manager.updateFees(300_000, 15_000, 14_000)
can_be_subsidized = fee_sub.tokenAmountCanBeSubsidized(
target_chain_id, token_address, transfer_amount
)
assert can_be_subsidized is False

token.transfer(fee_sub.address, 100_000_000)
can_be_subsidized = fee_sub.tokenAmountCanBeSubsidized(
target_chain_id, token_address, transfer_amount
)
assert can_be_subsidized is True


def test_enable_disable_token(fee_sub, deployer, request_manager):
token2 = deployer.deploy(ape.project.MintableToken, request_manager.address)
fee_sub.setMinimumAmount(token2.address, 5)
Expand Down
25 changes: 25 additions & 0 deletions contracts/contracts/FeeSub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,31 @@ contract FeeSub is Ownable {
token.safeTransfer(recipient, amount);
}

function tokenAmountCanBeSubsidized(
bilbeyt marked this conversation as resolved.
Show resolved Hide resolved
GabrielBuragev marked this conversation as resolved.
Show resolved Hide resolved
uint256 targetChainId,
address tokenAddress,
uint256 amount
) public view returns (bool) {
uint256 minimumAmount = minimumAmounts[tokenAddress];

if (minimumAmount == 0 || minimumAmount > amount) {
return false;
}

uint256 tokenBalance = IERC20(tokenAddress).balanceOf(address(this));
uint256 totalFee = requestManager.totalFee(
targetChainId,
tokenAddress,
amount
);

if (tokenBalance < totalFee) {
return false;
}

return true;
}

function setMinimumAmount(
address tokenAddress,
uint256 amount
Expand Down
7 changes: 5 additions & 2 deletions frontend/config/chains/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ export class ChainMetadata {
readonly explorerUrl: string;
readonly rpcUrl: string;
readonly name: string;
readonly imageUrl?: string;
readonly tokenSymbols: Array<string>;
readonly nativeCurrency?: NativeCurrency;
readonly internalRpcUrl: string;
readonly feeSubAddress?: string;
readonly nativeCurrency?: NativeCurrency;
readonly imageUrl?: string;
readonly disabled?: boolean;
readonly disabled_reason?: string;
readonly hidden?: boolean;
Expand All @@ -26,6 +27,7 @@ export class ChainMetadata {
this.tokenSymbols = data.tokenSymbols ?? [];
this.nativeCurrency = data.nativeCurrency;
this.internalRpcUrl = data.internalRpcUrl;
this.feeSubAddress = data.feeSubAddress;
this.disabled = data.disabled;
this.disabled_reason = data.disabled_reason;
this.hidden = data.hidden;
Expand Down Expand Up @@ -65,6 +67,7 @@ export type ChainMetadataData = {
tokenSymbols: Array<string>;
internalRpcUrl: string;
nativeCurrency?: NativeCurrency;
feeSubAddress?: string;
imageUrl?: string;
disabled?: boolean;
disabled_reason?: string;
Expand Down
6 changes: 6 additions & 0 deletions frontend/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export const configSchema: JSONSchemaType<BeamerConfig> = {
minLength: 42,
maxLength: 42,
},
feeSubAddress: {
type: 'string',
minLength: 42,
maxLength: 42,
nullable: true,
},
disabled: {
type: 'boolean',
nullable: true,
Expand Down
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"build": "yarn configure && vite build",
"preview": "vite preview",
"configure": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\", \"target\": \"es2021\"}' ts-node ./config/configure.ts && npx prettier --write config/**/*.json",
"generate-types": "typechain --target=ethers-v5 --glob='./node_modules/@beamer-bridge/deployments/dist/abis/**/!(deployment).json' --out-dir=./src/types/ethers-contracts/",
"generate-types": "yarn generate-beamer-types && yarn generate-external-types",
"generate-beamer-types": "typechain --target=ethers-v5 --glob='./node_modules/@beamer-bridge/deployments/dist/abis/**/!(deployment).json' --out-dir=./src/types/ethers-contracts/ ",
"generate-external-types": "typechain --target=ethers-v5 --glob='./src/assets/abi/**/*.json' --out-dir=./src/types/ethers-contracts/",
"check-types": "tsc --noEmit",
"eslint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --max-warnings 0 .",
"eslint:fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix .",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/actions/transfers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './allowance-information';
export * from './request-fulfillment';
export * from './request-information';
export * from './subsidized-transfer';
export * from './transaction-information';
export * from './transfer';
49 changes: 49 additions & 0 deletions frontend/src/actions/transfers/subsidized-transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { IEthereumProvider } from '@/services/web3-provider';
import { UInt256 } from '@/types/uint-256';

import { type TransferData, Transfer } from './transfer';

export class SubsidizedTransfer extends Transfer {
readonly feeSubAddress: string;
readonly originalRequestManagerAddress: string;

constructor(data: SubsidizedTransferData) {
if (!data.sourceChain.feeSubAddress) {
throw new Error('Please provide a fee subsidy contract address.');
}
super(data);
this.originalRequestManagerAddress = data.sourceChain.requestManagerAddress;
this.feeSubAddress = data.sourceChain.feeSubAddress;
Object.assign(this.fees.uint256, this.fees.uint256.multiply(new UInt256(0)));
}

async ensureTokenAllowance(provider: IEthereumProvider): Promise<void> {
if (this.feeSubAddress) {
this.sourceChain.requestManagerAddress = this.feeSubAddress;
}
await super.ensureTokenAllowance(provider);
this.sourceChain.requestManagerAddress = this.originalRequestManagerAddress;
}

async sendRequestTransaction(provider: IEthereumProvider): Promise<void> {
if (this.feeSubAddress) {
this.sourceChain.requestManagerAddress = this.feeSubAddress;
}
await super.sendRequestTransaction(provider);
this.sourceChain.requestManagerAddress = this.originalRequestManagerAddress;
}

public encode(): SubsidizedTransferData {
const encodedTransferData = super.encode();
return {
...encodedTransferData,
feeSubAddress: this.feeSubAddress,
originalRequestManagerAddress: this.originalRequestManagerAddress,
};
}
}

export type SubsidizedTransferData = TransferData & {
GabrielBuragev marked this conversation as resolved.
Show resolved Hide resolved
feeSubAddress: string;
originalRequestManagerAddress: string;
};
3 changes: 3 additions & 0 deletions frontend/src/actions/transfers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { SubsidizedTransferData, TransferData } from '.';

export type ExtendedTransferData = SubsidizedTransferData & TransferData;
100 changes: 100 additions & 0 deletions frontend/src/assets/abi/FeeSub.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
[
{
"inputs": [{ "internalType": "address", "name": "_requestManager", "type": "address" }],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" },
{ "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" }
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [
{ "internalType": "uint256", "name": "targetChainId", "type": "uint256" },
{ "internalType": "address", "name": "sourceTokenAddress", "type": "address" },
{ "internalType": "address", "name": "targetTokenAddress", "type": "address" },
{ "internalType": "address", "name": "targetAddress", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" },
{ "internalType": "uint256", "name": "validityPeriod", "type": "uint256" }
],
"name": "createRequest",
"outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "token", "type": "address" }],
"name": "minimumAmounts",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "requestManager",
"outputs": [{ "internalType": "contract RequestManager", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "bytes32", "name": "requestId", "type": "bytes32" }],
"name": "senders",
"outputs": [{ "internalType": "address", "name": "sender", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "tokenAddress", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "setMinimumAmount",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "targetChainId", "type": "uint256" },
{ "internalType": "address", "name": "tokenAddress", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "tokenAmountCanBeSubsidized",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "bytes32", "name": "requestId", "type": "bytes32" }],
"name": "withdrawExpiredRequest",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
1 change: 1 addition & 0 deletions frontend/src/components/RequestSourceInputs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ const { amount: requestFeeAmount, loading: requestFeeLoading } = useRequestFee(
computed(() => selectedSourceChain.value?.value.internalRpcUrl),
computed(() => selectedSourceChain.value?.value.requestManagerAddress),
selectedTokenAmount,
computed(() => selectedSourceChain.value?.value),
computed(() => props.targetChain?.value),
true,
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/composables/useChainSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function getChainSelectorOption(
imageUrl: chains[chainId].imageUrl,
nativeCurrency: chains[chainId].nativeCurrency,
internalRpcUrl: chains[chainId].internalRpcUrl,
feeSubAddress: chains[chainId].feeSubAddress,
disabled: chains[chainId].disabled,
disabled_reason: chains[chainId].disabled_reason,
hidden: chains[chainId].hidden,
Expand Down
21 changes: 17 additions & 4 deletions frontend/src/composables/useMaxTransferableTokenAmount.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Ref } from 'vue';
import { ref, watch } from 'vue';

import { amountCanBeSubsidized } from '@/services/transactions/fee-sub';
import { getAmountBeforeFees } from '@/services/transactions/request-manager';
import type { Chain } from '@/types/data';
import { TokenAmount } from '@/types/token-amount';
Expand All @@ -19,12 +20,24 @@ export function useMaxTransferableTokenAmount(
targetChain: Chain,
) {
try {
const transferableAmount = await getAmountBeforeFees(
const canBeSubsidized = await amountCanBeSubsidized(
sourceChain,
targetChain,
balance.token,
balance,
sourceChain.internalRpcUrl,
sourceChain.requestManagerAddress,
targetChain.identifier,
);

let transferableAmount;
if (canBeSubsidized) {
transferableAmount = balance.uint256;
} else {
transferableAmount = await getAmountBeforeFees(
balance,
sourceChain.internalRpcUrl,
sourceChain.requestManagerAddress,
targetChain.identifier,
);
}
maxTransferableTokenAmount.value = TokenAmount.new(transferableAmount, balance.token);
} catch (e) {
maxTransferableTokenAmount.value = undefined;
Expand Down
25 changes: 20 additions & 5 deletions frontend/src/composables/useRequestFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import type { Ref } from 'vue';
import { ref, watch } from 'vue';

import { useDebouncedTask } from '@/composables/useDebouncedTask';
import { amountCanBeSubsidized } from '@/services/transactions/fee-sub';
import { getRequestFee } from '@/services/transactions/request-manager';
import type { Chain } from '@/types/data';
import { TokenAmount } from '@/types/token-amount';
import { UInt256 } from '@/types/uint-256';

export function useRequestFee(
rpcUrl: Ref<string | undefined>,
requestManagerAddress: Ref<string | undefined>,
requestAmount: Ref<TokenAmount | undefined>,
sourceChain: Ref<Chain | undefined>,
targetChain: Ref<Chain | undefined>,
debounced?: boolean,
debouncedDelay = 500,
Expand All @@ -26,20 +29,32 @@ export function useRequestFee(
!rpcUrl.value ||
!requestManagerAddress.value ||
!requestAmount.value ||
!targetChain.value
!targetChain.value ||
!sourceChain.value
) {
amount.value = undefined;
loading.value = false;
return;
}

try {
const requestFee = await getRequestFee(
rpcUrl.value,
requestManagerAddress.value,
const canBeSubsdized = await amountCanBeSubsidized(
sourceChain.value,
targetChain.value,
requestAmount.value.token,
requestAmount.value,
targetChain.value.identifier,
);
let requestFee;
if (canBeSubsdized) {
requestFee = new UInt256(0);
} else {
requestFee = await getRequestFee(
rpcUrl.value,
requestManagerAddress.value,
requestAmount.value,
targetChain.value.identifier,
);
}
amount.value = TokenAmount.new(requestFee, requestAmount.value.token);
} catch (exception: unknown) {
const errorMessage = (exception as { message?: string }).message;
Expand Down
Loading