Skip to content

Commit

Permalink
contract: get token balances util contract
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewliu08 committed Nov 5, 2024
1 parent 0efd4d1 commit 239f552
Show file tree
Hide file tree
Showing 13 changed files with 841 additions and 10 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions packages/contract/script/DeployTokenBalanceUtils.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;

import "forge-std/Script.sol";

import "../src/TokenBalanceUtils.sol";
import "./Constants.s.sol";

contract DeployTokenBalanceUtils is Script {
function run() public {
vm.startBroadcast();

address owner = tx.origin;
console.log("owner:", owner);

address tokenBalanceUtils = CREATE3.deploy(
keccak256("TokenBalanceUtils-test4"),
abi.encodePacked(
type(TokenBalanceUtils).creationCode,
abi.encode(owner)
)
);

vm.stopBroadcast();

console.log(
"token balance utils deployed at address:",
tokenBalanceUtils
);
}

// Exclude from forge coverage
function test() public {}
}
5 changes: 3 additions & 2 deletions packages/contract/script/deployV2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ set -e
# ETHERSCAN_API_KEY_... for each target chain

SCRIPTS=(
# CREATE3
# Utils
# "script/DeployCreate3Factory.s.sol"
# "script/DeployTokenBalanceUtils.s.sol"

# Daimo Pay.
# Daimo Pay
# "script/pay/DeployDaimoPayAcrossBridger.s.sol"
# "script/pay/DeployDaimoPayCCTPBridger.s.sol"
# "script/pay/DeployDaimoPayAxelarBridger.s.sol"
Expand Down
61 changes: 61 additions & 0 deletions packages/contract/src/TokenBalanceUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.12;

import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";

contract TokenBalanceUtils is Ownable {
IERC20[] public tokens;

constructor(address initialOwner) Ownable(initialOwner) {}

function getAllTokens() public view returns (IERC20[] memory) {
return tokens;
}

function setTokens(IERC20[] memory _tokens) public onlyOwner {
tokens = _tokens;
}

/**
* @notice Get the balances for all saved tokens and the balance of the
* native asset for an owner
* @param owner The owner of the tokens
* @return balances An array of balances, where the last element is the
* balance of the native asset
*/
function getTokenBalances(
address owner
) public view returns (uint256[] memory balances) {
uint256 n = tokens.length;

balances = new uint256[](n + 1);
for (uint256 i = 0; i < n; ++i) {
balances[i] = tokens[i].balanceOf(owner);
}

balances[n] = owner.balance;
}

/**
* @notice Get the balances for a custom list of tokens and the balance of
* the native asset for an owner
* @param owner The owner of the tokens
* @param tokenList The list of token addresses to get the balance of
* @return balances An array of balances, where the last element is the
* balance of the native asset
*/
function getTokenBalancesBatch(
address owner,
IERC20[] calldata tokenList
) public view returns (uint256[] memory balances) {
uint256 n = tokenList.length;

balances = new uint256[](n + 1);
for (uint256 i = 0; i < n; ++i) {
balances[i] = tokenList[i].balanceOf(owner);
}

balances[n] = owner.balance;
}
}
131 changes: 128 additions & 3 deletions packages/daimo-contract/script/gen-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import fs from "node:fs/promises";
import { getAddress, Hex, isAddress } from "viem";
import {
Address,
createPublicClient,
erc20Abi,
getAddress,
Hex,
http,
isAddress,
MulticallResponse,
zeroAddress,
} from "viem";

import { DAv2Chain, getDAv2Chain } from "../src";
import {
DAv2Chain,
getAlchemyTransportUrl,
getDAv2Chain,
getViemChainById,
} from "../src";
import { baseUSDbC, baseUSDC, ForeignToken } from "../src/foreignToken";
// eslint-disable-next-line import/order
import { assert } from "./util";
Expand Down Expand Up @@ -31,10 +46,17 @@ interface TokenListToken {
logoURI?: string;
}

const alchemyApiKey = "";

/**
* Generate a token list across all supported chains.
*/
async function main() {
if (!alchemyApiKey) {
console.error("ALCHEMY_API_KEY is not set");
process.exit(1);
}

console.log("Generating token list...");

const foreignTokens: ForeignToken[] = [];
Expand All @@ -49,7 +71,13 @@ async function main() {
.filter((token) => token != null) as ForeignToken[];
console.log(`Loaded ${tokens.length} tokens for ${chain.name} from ${url}`);

foreignTokens.push(...tokens);
const validatedTokens = await erc20Validation(chain.chainId, tokens);

foreignTokens.push(...validatedTokens);

console.log(
`Got ${validatedTokens.length} validated tokens and ${tokens.length} total tokens for ${chain.name}`,
);
}

console.log(`Writing ${foreignTokens.length} tokens to tokens.json`);
Expand Down Expand Up @@ -98,6 +126,103 @@ function getForeignToken(
}
}

/**
* - Remove tokens whose contract does not implement the ERC20 interface.
* - Remove tokens whose decimals on-chain don't match the decimals returned by
* Coingecko.
*/
async function erc20Validation(
chainId: number,
tokens: ForeignToken[],
): Promise<ForeignToken[]> {
const client = createPublicClient({
chain: getViemChainById(chainId),
transport: http(getAlchemyTransportUrl(chainId, alchemyApiKey)),
});

const dummyEOA = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const dummySpender = "0xb7A593EC62dc447eef23ea0e0B4d5144ac75ABC5";

const erc20ViewFunctions = (tokenAddress: Address) => [
{
abi: erc20Abi,
address: tokenAddress,
functionName: "name",
args: [],
},
{
abi: erc20Abi,
address: tokenAddress,
functionName: "symbol",
args: [],
},
{
abi: erc20Abi,
address: tokenAddress,
functionName: "decimals",
args: [],
},
{
abi: erc20Abi,
address: tokenAddress,
functionName: "totalSupply",
args: [],
},
{
abi: erc20Abi,
address: tokenAddress,
functionName: "balanceOf",
args: [dummyEOA],
},
{
abi: erc20Abi,
address: tokenAddress,
functionName: "allowance",
args: [dummyEOA, dummySpender],
},
];
const n = erc20ViewFunctions(zeroAddress).length;

const multicallContracts = tokens.flatMap((token) =>
erc20ViewFunctions(token.token),
);

const multicallResults = await client.multicall({
contracts: multicallContracts,
batchSize: 512,
allowFailure: true,
});

const filteredTokens = tokens
.map((token, index) => {
const results = multicallResults.slice(index * n, (index + 1) * n) as [
MulticallResponse<string, unknown, true>, // name
MulticallResponse<string, unknown, true>, // symbol
MulticallResponse<number, unknown, true>, // decimals
MulticallResponse<bigint, unknown, true>, // totalSupply
MulticallResponse<bigint, unknown, true>, // balanceOf
MulticallResponse<bigint, unknown, true>, // allowance
];

// If any of the calls were unsuccessful, filter the token out
for (let i = 0; i < results.length; i++) {
if (results[i].status !== "success") {
return null;
}
}

const decimals = results[2];
if (token.decimals !== decimals.result) {
return null;
}

return token;
})
.filter((out) => out != null);

return filteredTokens;
}

main()
.then(() => console.log("Done"))
.catch((e) => console.error(e));
114 changes: 114 additions & 0 deletions packages/daimo-contract/src/codegen/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7205,3 +7205,117 @@ export const testUsdcAbi = [
name: 'ERC20InvalidSpender',
},
] as const

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// TokenBalanceUtils
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

export const tokenBalanceUtilsAbi = [
{
type: 'constructor',
inputs: [
{ name: 'initialOwner', internalType: 'address', type: 'address' },
],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [],
name: 'getAllTokens',
outputs: [
{ name: '', internalType: 'contract IERC20[]', type: 'address[]' },
],
stateMutability: 'view',
},
{
type: 'function',
inputs: [{ name: 'owner', internalType: 'address', type: 'address' }],
name: 'getTokenBalances',
outputs: [
{ name: 'balances', internalType: 'uint256[]', type: 'uint256[]' },
],
stateMutability: 'view',
},
{
type: 'function',
inputs: [
{ name: 'owner', internalType: 'address', type: 'address' },
{
name: 'tokenList',
internalType: 'contract IERC20[]',
type: 'address[]',
},
],
name: 'getTokenBalancesBatch',
outputs: [
{ name: 'balances', internalType: 'uint256[]', type: 'uint256[]' },
],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'owner',
outputs: [{ name: '', internalType: 'address', type: 'address' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'renounceOwnership',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [
{ name: '_tokens', internalType: 'contract IERC20[]', type: 'address[]' },
],
name: 'setTokens',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
name: 'tokens',
outputs: [{ name: '', internalType: 'contract IERC20', type: 'address' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }],
name: 'transferOwnership',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'previousOwner',
internalType: 'address',
type: 'address',
indexed: true,
},
{
name: 'newOwner',
internalType: 'address',
type: 'address',
indexed: true,
},
],
name: 'OwnershipTransferred',
},
{
type: 'error',
inputs: [{ name: 'owner', internalType: 'address', type: 'address' }],
name: 'OwnableInvalidOwner',
},
{
type: 'error',
inputs: [{ name: 'account', internalType: 'address', type: 'address' }],
name: 'OwnableUnauthorizedAccount',
},
] as const
Loading

0 comments on commit 239f552

Please sign in to comment.