-
Notifications
You must be signed in to change notification settings - Fork 394
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
Untrusted assets should not block the addition of custom tokens #3491
Changes from 6 commits
5a849ea
5f36c19
dce74a9
ee820f5
12dce07
546f8c5
c83cf0e
7ce70ff
5897275
c77920a
0db9840
14414f4
b0969f4
b005153
bcc7579
760c3a4
c01bef5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ import { selectCurrentNetwork } from "./uiSelectors" | |
import { SwappableAsset, isSmartContractFungibleAsset } from "../../assets" | ||
import { sameNetwork } from "../../networks" | ||
import { | ||
canBeUsedForTransaction, | ||
isVerifiedAsset, | ||
isBuiltInNetworkBaseAsset, | ||
} from "../utils/asset-utils" | ||
import { RootState } from ".." | ||
|
@@ -29,7 +29,7 @@ export const selectSwapBuyAssets = createSelector( | |
): asset is SwappableAsset & { | ||
recentPrices: SingleAssetState["recentPrices"] | ||
} => { | ||
if (!canBeUsedForTransaction(asset)) { | ||
if (!isVerifiedAsset(asset)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're stacking enough conditions here that most of this function is noise. I wonder if we wouldn't be better served rewriting it as: return isVerifiedAsset(asset) && (
// Only list assets for the current network.
isBuiltInNetworkBaseAsset(asset, currentNetwork) ||
(isSmartContractFungibleAsset(asset) && sameNetwork(asset.homeNetwork, currentNetwork))
) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return false | ||
} | ||
if (isSmartContractFungibleAsset(asset)) { | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -377,6 +377,23 @@ export function isUnverifiedAssetByUser(asset: AnyAsset | undefined): boolean { | |||||||||
return false | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Check if an asset is verified. | ||||||||||
* The asset can be verified by us when it is trusted by default. | ||||||||||
* Untrusted asset can be manually verified by the user. | ||||||||||
* | ||||||||||
* Only verified assets can take part in wallet actions. | ||||||||||
* By actions is meant: | ||||||||||
* - doing an swap with this asset | ||||||||||
* - sending this asset to another address | ||||||||||
*/ | ||||||||||
export function isVerifiedAsset(asset: AnyAsset): boolean { | ||||||||||
if (!isEnabled(FeatureFlags.SUPPORT_UNVERIFIED_ASSET)) { | ||||||||||
return true | ||||||||||
} | ||||||||||
return isUntrustedAsset(asset) ? !isUnverifiedAssetByUser(asset) : true | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't extension/background/redux-slices/utils/asset-utils.ts Lines 371 to 374 in 546f8c5
I think if we dig through and do a bunch of really ugly K Maps of this1, we can simplify this assertion to: return isNetworkBaseAsset(asset) || !isUntrustedAsset(asset) I'm having a lot of difficulty reasoning my way through these though and I think it's because we're using a lot of negatives, and we've also muddled three concepts I want to circle back to: baseline trusted assets, trusted assets, and verified assets. I think we can define a very clear, stackable set of conditions that are easier to reason about with these:
In this scenario the negatives also have very clear meanings as the opposites of the above (
I strongly believe these three concepts are enough to handle all of what we're trying to address, and can all be stated in terms of positives ( Do you agree? Have I missed an edge case? I found about 4 while I was writing and rewriting this comment, so I wouldn't be surprised if I missed another 😅 FootnotesThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two things here:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has been brilliantly explained. Let's simplify this logic to make it clear and unambiguous. PR with a new concept #3528 |
||||||||||
} | ||||||||||
|
||||||||||
type AssetType = "base" | "erc20" | ||||||||||
|
||||||||||
export type AssetID = `${AssetType}/${string}` | ||||||||||
|
@@ -391,17 +408,6 @@ export const getAssetID = ( | |||||||||
return `erc20/${asset.contractAddress}` | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Assets that are untrusted and have not been verified by the user | ||||||||||
* should not be swapped or sent. | ||||||||||
*/ | ||||||||||
export function canBeUsedForTransaction(asset: AnyAsset): boolean { | ||||||||||
if (!isEnabled(FeatureFlags.SUPPORT_UNVERIFIED_ASSET)) { | ||||||||||
return true | ||||||||||
} | ||||||||||
return isUntrustedAsset(asset) ? !isUnverifiedAssetByUser(asset) : true | ||||||||||
} | ||||||||||
|
||||||||||
// FIXME Unify once asset similarity code is unified. | ||||||||||
export function isSameAsset(asset1?: AnyAsset, asset2?: AnyAsset): boolean { | ||||||||||
if (typeof asset1 === "undefined" || typeof asset2 === "undefined") { | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { SmartContractFungibleAsset } from "../../../assets" | ||
import { EVMNetwork } from "../../../networks" | ||
import { | ||
isUntrustedAsset, | ||
isVerifiedAsset, | ||
} from "../../../redux-slices/utils/asset-utils" | ||
import { HexString } from "../../../types" | ||
|
||
export const getAssetsByAddress = ( | ||
assets: SmartContractFungibleAsset[] | ||
): { | ||
[address: string]: SmartContractFungibleAsset | ||
} => { | ||
const activeAssetsByAddress = assets.reduce((agg, t) => { | ||
const newAgg = { | ||
...agg, | ||
} | ||
newAgg[t.contractAddress.toLowerCase()] = t | ||
return newAgg | ||
}, {} as { [address: string]: SmartContractFungibleAsset }) | ||
|
||
return activeAssetsByAddress | ||
} | ||
|
||
export const getActiveAssetsByAddressForNetwork = ( | ||
network: EVMNetwork, | ||
activeAssetsToTrack: SmartContractFungibleAsset[] | ||
): { | ||
[address: string]: SmartContractFungibleAsset | ||
} => { | ||
const networkActiveAssets = activeAssetsToTrack.filter( | ||
(asset) => asset.homeNetwork.chainID === network.chainID | ||
) | ||
|
||
return getAssetsByAddress(networkActiveAssets) | ||
} | ||
|
||
export function shouldRefreshKnownAsset( | ||
asset: SmartContractFungibleAsset, | ||
metadata: { | ||
discoveryTxHash?: { | ||
[address: HexString]: HexString | ||
} | ||
verified?: boolean | ||
} | ||
): boolean { | ||
const newDiscoveryTxHash = metadata?.discoveryTxHash | ||
const addressForDiscoveryTxHash = newDiscoveryTxHash | ||
? Object.keys(newDiscoveryTxHash)[0] | ||
: undefined | ||
const existingDiscoveryTxHash = addressForDiscoveryTxHash | ||
? asset.metadata?.discoveryTxHash?.[addressForDiscoveryTxHash] | ||
: undefined | ||
|
||
// If the discovery tx hash is not specified | ||
// or if it already exists in the asset, do not update the asset | ||
// Additionally, discovery tx Hash should only be added for untrusted assets. | ||
const allowAddDiscoveryTxHash = | ||
isUntrustedAsset(asset) && !(!newDiscoveryTxHash || existingDiscoveryTxHash) | ||
|
||
// Refresh a known unverified asset if it has been manually imported. | ||
// This check allows the user to add an asset from the unverified list. | ||
const isManuallyImported = metadata?.verified | ||
const allowVerifyAssetByManualImport = | ||
!isVerifiedAsset(asset) && isManuallyImported | ||
|
||
return allowVerifyAssetByManualImport || allowAddDiscoveryTxHash | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { shouldRefreshKnownAsset } from ".." | ||
import { createSmartContractAsset } from "../../../../tests/factories" | ||
import * as featureFlags from "../../../../features" | ||
|
||
const ADDRESS = "0x0000000000000000000000000000000000000000" | ||
const DISCOVERY_TX_HASH = "0x0000000000000000000000000000000000000000" | ||
const METADATA_TX = { | ||
discoveryTxHash: { | ||
[ADDRESS]: DISCOVERY_TX_HASH, | ||
}, | ||
} | ||
const METADATA_VERIFIED = { | ||
verified: true, | ||
} | ||
|
||
const TRUSTED_ASSET = createSmartContractAsset() | ||
const UNTRUSTED_ASSET = createSmartContractAsset({ | ||
metadata: { tokenLists: [] }, | ||
}) | ||
|
||
describe("IndexingService utils", () => { | ||
describe("shouldRefreshKnownAsset", () => { | ||
beforeAll(() => { | ||
jest.spyOn(featureFlags, "isEnabled").mockImplementation(() => true) | ||
}) | ||
|
||
it("Refresh the untrusted asset if manually imported", () => { | ||
const shouldBeRefreshed = shouldRefreshKnownAsset( | ||
UNTRUSTED_ASSET, | ||
METADATA_VERIFIED | ||
) | ||
|
||
expect(shouldBeRefreshed).toBe(true) | ||
}) | ||
|
||
it("Not refresh the untrusted asset if not manually imported", () => { | ||
const shouldBeRefreshed = shouldRefreshKnownAsset(UNTRUSTED_ASSET, {}) | ||
|
||
expect(shouldBeRefreshed).toBe(false) | ||
}) | ||
|
||
it("Not refresh the trusted asset if not manually imported", () => { | ||
const shouldBeRefreshed = shouldRefreshKnownAsset(TRUSTED_ASSET, {}) | ||
|
||
expect(shouldBeRefreshed).toBe(false) | ||
}) | ||
|
||
// This state is not quite possible in the app because the user is not able to manually import a trusted asset that exists. | ||
it("Not refresh the trusted asset if manually imported", () => { | ||
const shouldBeRefreshed = shouldRefreshKnownAsset( | ||
TRUSTED_ASSET, | ||
METADATA_VERIFIED | ||
) | ||
|
||
expect(shouldBeRefreshed).toBe(false) | ||
}) | ||
|
||
it("Refresh the untrusted asset if it does not already have a discovered tx hash", () => { | ||
const shouldBeRefreshed = shouldRefreshKnownAsset( | ||
UNTRUSTED_ASSET, | ||
METADATA_TX | ||
) | ||
|
||
expect(shouldBeRefreshed).toBe(true) | ||
}) | ||
|
||
it("Not refresh the trusted asset if it does not already have a discovered tx hash", () => { | ||
const shouldBeRefreshed = shouldRefreshKnownAsset( | ||
TRUSTED_ASSET, | ||
METADATA_TX | ||
) | ||
|
||
expect(shouldBeRefreshed).toBe(false) | ||
}) | ||
|
||
it("Not refresh the untrusted asset if it does already have a discovered tx hash", () => { | ||
const asset = { | ||
...UNTRUSTED_ASSET, | ||
metadata: { ...UNTRUSTED_ASSET, ...METADATA_TX }, | ||
} | ||
const shouldBeRefreshed = shouldRefreshKnownAsset(asset, METADATA_TX) | ||
|
||
expect(shouldBeRefreshed).toBe(false) | ||
}) | ||
|
||
it("Not refresh the trusted asset if it does already have a discovered tx hash", () => { | ||
const asset = { | ||
...TRUSTED_ASSET, | ||
metadata: { ...TRUSTED_ASSET, ...METADATA_TX }, | ||
} | ||
const shouldBeRefreshed = shouldRefreshKnownAsset(asset, METADATA_TX) | ||
|
||
expect(shouldBeRefreshed).toBe(false) | ||
}) | ||
|
||
it("Not refresh the trusted asset if discovered tx hash is not specified", () => { | ||
const asset = { | ||
...TRUSTED_ASSET, | ||
metadata: { ...TRUSTED_ASSET, ...METADATA_TX }, | ||
} | ||
|
||
expect(shouldRefreshKnownAsset(TRUSTED_ASSET, {})).toBe(false) | ||
expect(shouldRefreshKnownAsset(asset, {})).toBe(false) | ||
}) | ||
it("Not refresh the untrusted asset if discovered tx hash is not specified", () => { | ||
const asset = { | ||
...UNTRUSTED_ASSET, | ||
metadata: { ...UNTRUSTED_ASSET, ...METADATA_TX }, | ||
} | ||
|
||
expect(shouldRefreshKnownAsset(UNTRUSTED_ASSET, {})).toBe(false) | ||
expect(shouldRefreshKnownAsset(asset, {})).toBe(false) | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic and naming seems a little clearer like this, I think, but I'm not 100% sure it's all accurate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
c83cf0e