diff --git a/typescript/ica/.gitignore b/typescript/ica/.gitignore new file mode 100644 index 000000000..afde817eb --- /dev/null +++ b/typescript/ica/.gitignore @@ -0,0 +1,9 @@ +artifacts/ +cache/ +coverage/ +coverage.json +dist/ +node_modules/ +types/ +*.swp +.yarn/install-state.gz diff --git a/typescript/ica/README.md b/typescript/ica/README.md new file mode 100644 index 000000000..b51749cfa --- /dev/null +++ b/typescript/ica/README.md @@ -0,0 +1,6 @@ +This package provides smart contracts with "sovereignty" on remote Abacus chains via operating interchain accounts. +An interchain account is a smart contract that is deployed on a remote chain and is controlled exclusively by the deploying local account. +Interchain accounts provide developers with a [transparent multicall API](./contracts/OwnableMulticall.sol) to remote smart contracts. +This avoids the need to deploy application specific smart contracts on remote chains while simultaneously enabling crosschain composability. + +See [IBC Interchain Accounts](https://github.com/cosmos/ibc/blob/main/spec/app/ics-027-interchain-accounts/README.md) for the Cosmos ecosystem equivalent. diff --git a/typescript/ica/contracts/InterchainAccountRouter.sol b/typescript/ica/contracts/InterchainAccountRouter.sol new file mode 100644 index 000000000..1839aab3a --- /dev/null +++ b/typescript/ica/contracts/InterchainAccountRouter.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import {OwnableMulticall, Call} from "./OwnableMulticall.sol"; + +// ============ External Imports ============ +import {Router} from "@abacus-network/app/contracts/Router.sol"; +import {TypeCasts} from "@abacus-network/core/contracts/libs/TypeCasts.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/* + * @title The Hello World App + * @dev You can use this simple app as a starting point for your own application. + */ +contract InterchainAccountRouter is Router { + bytes constant bytecode = type(OwnableMulticall).creationCode; + bytes32 constant bytecodeHash = bytes32(keccak256(bytecode)); + + constructor( + address _abacusConnectionManager, + address _interchainGasPaymaster + ) { + // Transfer ownership of the contract to deployer + _transferOwnership(msg.sender); + // Set the addresses for the ACM and IGP + // Alternatively, this could be done later in an initialize method + _setAbacusConnectionManager(_abacusConnectionManager); + _setInterchainGasPaymaster(_interchainGasPaymaster); + } + + function dispatch(uint32 _destinationDomain, Call[] calldata calls) + external + { + _dispatch(_destinationDomain, abi.encode(msg.sender, calls)); + } + + function getInterchainAccount(uint32 _origin, address _sender) + public + view + returns (address) + { + return _getInterchainAccount(_salt(_origin, _sender)); + } + + function getDeployedInterchainAccount(uint32 _origin, address _sender) + public + returns (OwnableMulticall) + { + bytes32 salt = _salt(_origin, _sender); + address interchainAccount = _getInterchainAccount(salt); + if (!Address.isContract(interchainAccount)) { + interchainAccount = Create2.deploy(0, salt, bytecode); + } + return OwnableMulticall(interchainAccount); + } + + function _salt(uint32 _origin, address _sender) + internal + pure + returns (bytes32) + { + return bytes32(abi.encodePacked(_origin, _sender)); + } + + function _getInterchainAccount(bytes32 salt) + internal + view + returns (address) + { + return Create2.computeAddress(salt, bytecodeHash); + } + + function _handle( + uint32 _origin, + bytes32, // router sender + bytes memory _message + ) internal override { + (address sender, Call[] memory calls) = abi.decode( + _message, + (address, Call[]) + ); + getDeployedInterchainAccount(_origin, sender).proxyCalls(calls); + } +} diff --git a/typescript/ica/contracts/OwnableMulticall.sol b/typescript/ica/contracts/OwnableMulticall.sol new file mode 100644 index 000000000..20a4b4d3e --- /dev/null +++ b/typescript/ica/contracts/OwnableMulticall.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +// ============ External Imports ============ + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +struct Call { + address to; + bytes data; +} + +/* + * @title OwnableMulticall + * @dev Allows only only address to execute calls to other contracts + */ +contract OwnableMulticall is OwnableUpgradeable { + constructor() { + _transferOwnership(msg.sender); + } + + function proxyCalls(Call[] calldata calls) external onlyOwner { + for (uint256 i = 0; i < calls.length; i += 1) { + (bool success, bytes memory returnData) = calls[i].to.call( + calls[i].data + ); + if (!success) { + assembly { + revert(add(returnData, 32), returnData) + } + } + } + } +} diff --git a/typescript/ica/contracts/TestRecipient.sol b/typescript/ica/contracts/TestRecipient.sol new file mode 100644 index 000000000..7e6cd68c3 --- /dev/null +++ b/typescript/ica/contracts/TestRecipient.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +contract TestRecipient { + address public lastSender; + bytes public lastData; + + function foo(bytes calldata data) external { + lastSender = msg.sender; + lastData = data; + } +} diff --git a/typescript/ica/hardhat.config.ts b/typescript/ica/hardhat.config.ts new file mode 100644 index 000000000..d25cffce2 --- /dev/null +++ b/typescript/ica/hardhat.config.ts @@ -0,0 +1,26 @@ +import '@nomiclabs/hardhat-ethers'; +import '@nomiclabs/hardhat-waffle'; +import '@typechain/hardhat'; +import 'hardhat-gas-reporter'; +import 'solidity-coverage'; + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +module.exports = { + solidity: { + compilers: [ + { + version: '0.8.16', + }, + ], + }, + gasReporter: { + currency: 'USD', + }, + typechain: { + outDir: './types', + target: 'ethers-v5', + alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads? + }, +}; diff --git a/typescript/ica/package.json b/typescript/ica/package.json new file mode 100644 index 000000000..642ecfe4f --- /dev/null +++ b/typescript/ica/package.json @@ -0,0 +1,66 @@ +{ + "name": "@abacus-network/interchain-accounts", + "description": "A router middleware for interchain accounts", + "version": "0.1.0", + "dependencies": { + "@abacus-network/app": "^0.4.1", + "@abacus-network/sdk": "^0.4.1", + "@abacus-network/utils": "^0.4.1", + "@openzeppelin/contracts-upgradeable": "^4.6.0", + "ethers": "^5.6.8" + }, + "devDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^1.0.3", + "@nomicfoundation/hardhat-toolbox": "^1.0.2", + "@nomiclabs/hardhat-ethers": "^2.0.5", + "@nomiclabs/hardhat-waffle": "^2.0.2", + "@trivago/prettier-plugin-sort-imports": "^3.2.0", + "@typechain/ethers-v5": "10.0.0", + "@typechain/hardhat": "^6.0.0", + "@types/mocha": "^9.1.0", + "@typescript-eslint/eslint-plugin": "^5.27.0", + "@typescript-eslint/parser": "^5.27.0", + "chai": "^4.3.0", + "eslint": "^8.16.0", + "eslint-config-prettier": "^8.5.0", + "ethereum-waffle": "^3.4.4", + "hardhat": "^2.8.4", + "hardhat-gas-reporter": "^1.0.7", + "prettier": "^2.4.1", + "prettier-plugin-solidity": "^1.0.0-beta.5", + "solhint": "^3.3.2", + "solhint-plugin-prettier": "^0.0.5", + "solidity-coverage": "^0.7.14", + "ts-node": "^10.8.0", + "typechain": "8.0.0", + "typescript": "^4.7.2" + }, + "files": [ + "/dist", + "/contracts" + ], + "homepage": "https://www.useabacus.network", + "keywords": [ + "Abacus", + "Interchain Accounts", + "Solidity", + "Typescript" + ], + "license": "Apache-2.0", + "main": "dist/src/index.js", + "packageManager": "yarn@3.2.0", + "repository": { + "type": "git", + "url": "https://github.com/abacus-network/abacus-app-template" + }, + "scripts": { + "build": "hardhat compile && tsc", + "clean": "hardhat clean && rm -rf dist cache src/types", + "coverage": "hardhat coverage", + "lint": "eslint . --ext .ts", + "prettier": "prettier --write ./contracts ./src", + "test": "hardhat test ./test/*.test.ts", + "sync": "ts-node scripts/sync-with-template-repo.ts" + }, + "types": "dist/src/index.d.ts" +} diff --git a/typescript/ica/src/contracts.ts b/typescript/ica/src/contracts.ts new file mode 100644 index 000000000..75040124f --- /dev/null +++ b/typescript/ica/src/contracts.ts @@ -0,0 +1,16 @@ +import { RouterContracts, RouterFactories } from '@abacus-network/sdk'; + +import { + InterchainAccountRouter, + InterchainAccountRouter__factory, +} from '../types'; + +export type InterchainAccountFactories = + RouterFactories; + +export const InterchainAccountFactories: InterchainAccountFactories = { + router: new InterchainAccountRouter__factory(), +}; + +export type InterchainAccountContracts = + RouterContracts; diff --git a/typescript/ica/src/deploy.ts b/typescript/ica/src/deploy.ts new file mode 100644 index 000000000..471cc7efe --- /dev/null +++ b/typescript/ica/src/deploy.ts @@ -0,0 +1,44 @@ +import { + AbacusCore, + AbacusRouterDeployer, + ChainMap, + ChainName, + MultiProvider, + RouterConfig, +} from '@abacus-network/sdk'; + +import { + InterchainAccountContracts, + InterchainAccountFactories, +} from './contracts'; + +export type InterchainAccountConfig = RouterConfig; + +export class InterchainAccountDeployer< + Chain extends ChainName, +> extends AbacusRouterDeployer< + Chain, + InterchainAccountConfig, + InterchainAccountContracts, + InterchainAccountFactories +> { + constructor( + multiProvider: MultiProvider, + configMap: ChainMap, + protected core: AbacusCore, + ) { + super(multiProvider, configMap, InterchainAccountFactories, {}); + } + + // Custom contract deployment logic can go here + // If no custom logic is needed, call deployContract for the router + async deployContracts(chain: Chain, config: InterchainAccountConfig) { + const router = await this.deployContract(chain, 'router', [ + config.abacusConnectionManager, + config.interchainGasPaymaster, + ]); + return { + router, + }; + } +} diff --git a/typescript/ica/test/accounts.test.ts b/typescript/ica/test/accounts.test.ts new file mode 100644 index 000000000..d499b5256 --- /dev/null +++ b/typescript/ica/test/accounts.test.ts @@ -0,0 +1,73 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { + ChainMap, + ChainNameToDomainId, + MultiProvider, + RouterConfig, + TestChainNames, + TestCoreApp, + TestCoreDeployer, + getChainToOwnerMap, + getTestMultiProvider, + testChainConnectionConfigs, +} from '@abacus-network/sdk'; + +import { InterchainAccountDeployer } from '../src/deploy'; +import { InterchainAccountRouter, TestRecipient__factory } from '../types'; + +describe('InterchainAccountRouter', async () => { + const localChain = 'test1'; + const remoteChain = 'test2'; + const localDomain = ChainNameToDomainId[localChain]; + const remoteDomain = ChainNameToDomainId[remoteChain]; + + let signer: SignerWithAddress; + let local: InterchainAccountRouter; + let remote: InterchainAccountRouter; + let multiProvider: MultiProvider; + let coreApp: TestCoreApp; + let config: ChainMap; + + before(async () => { + [signer] = await ethers.getSigners(); + + multiProvider = getTestMultiProvider(signer); + + const coreDeployer = new TestCoreDeployer(multiProvider); + const coreContractsMaps = await coreDeployer.deploy(); + coreApp = new TestCoreApp(coreContractsMaps, multiProvider); + config = coreApp.extendWithConnectionClientConfig( + getChainToOwnerMap(testChainConnectionConfigs, signer.address), + ); + }); + + beforeEach(async () => { + const InterchainAccount = new InterchainAccountDeployer( + multiProvider, + config, + coreApp, + ); + const contracts = await InterchainAccount.deploy(); + + local = contracts[localChain].router; + remote = contracts[remoteChain].router; + }); + + it('forwards calls from interchain account', async () => { + const recipientF = new TestRecipient__factory(signer); + const recipient = await recipientF.deploy(); + const fooData = '0x12'; + const data = recipient.interface.encodeFunctionData('foo', [fooData]); + const icaAddress = await remote.getInterchainAccount( + localDomain, + signer.address, + ); + await local.dispatch(remoteDomain, [{ to: recipient.address, data }]); + await coreApp.processMessages(); + expect(await recipient.lastData()).to.eql(fooData); + expect(await recipient.lastSender()).to.eql(icaAddress); + }); +}); diff --git a/typescript/ica/tsconfig.json b/typescript/ica/tsconfig.json new file mode 100644 index 000000000..d59b30b84 --- /dev/null +++ b/typescript/ica/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist/", + "rootDir": "./", + "noImplicitAny": false, + }, + "exclude": ["./node_modules/", "./dist/", "./src/types/hardhat.d.ts"], + "include": ["./src/", "./test", "./scripts"], + "files": ["hardhat.config.ts"] +} diff --git a/typescript/interchain/scripts/deploy.ts b/typescript/interchain/scripts/deploy.ts new file mode 100644 index 000000000..f861a4e31 --- /dev/null +++ b/typescript/interchain/scripts/deploy.ts @@ -0,0 +1,48 @@ +import { Wallet } from 'ethers'; + +import { + AbacusCore, + MultiProvider, + chainConnectionConfigs, + getChainToOwnerMap, + objMap, + serializeContracts, +} from '@abacus-network/sdk'; + +import { InterchainAccountDeployer } from '../src/deploy'; + +export const prodConfigs = { + alfajores: chainConnectionConfigs.alfajores, + fuji: chainConnectionConfigs.fuji, + bsctestnet: chainConnectionConfigs.bsctestnet, +}; + +/* eslint-disable no-console */ +async function main() { + console.info('Getting signer'); + const signer = new Wallet('pkey'); + + console.info('Preparing utilities'); + const chainProviders = objMap(prodConfigs, (_, config) => ({ + provider: config.provider, + confirmations: config.confirmations, + overrides: config.overrides, + signer: new Wallet('pkey', config.provider), + })); + const multiProvider = new MultiProvider(chainProviders); + + const core = AbacusCore.fromEnvironment('testnet2', multiProvider); + const config = core.extendWithConnectionClientConfig( + getChainToOwnerMap(prodConfigs, signer.address), + ); + + const deployer = new InterchainAccountDeployer(multiProvider, config, core); + const chainToContracts = await deployer.deploy(); + const addresses = serializeContracts(chainToContracts); + console.info('===Contract Addresses==='); + console.info(JSON.stringify(addresses)); +} + +main() + .then(() => console.info('Deploy complete')) + .catch(console.error); diff --git a/yarn.lock b/yarn.lock index d36a3d070..2602d6e60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,7 +5,7 @@ __metadata: version: 6 cacheKey: 8 -"@abacus-network/app@0.4.1, @abacus-network/app@workspace:solidity/app": +"@abacus-network/app@0.4.1, @abacus-network/app@^0.4.1, @abacus-network/app@workspace:solidity/app": version: 0.0.0-use.local resolution: "@abacus-network/app@workspace:solidity/app" dependencies: @@ -143,6 +143,42 @@ __metadata: languageName: unknown linkType: soft +"@abacus-network/interchain-accounts@workspace:typescript/ica": + version: 0.0.0-use.local + resolution: "@abacus-network/interchain-accounts@workspace:typescript/ica" + dependencies: + "@abacus-network/app": ^0.4.1 + "@abacus-network/sdk": ^0.4.1 + "@abacus-network/utils": ^0.4.1 + "@nomicfoundation/hardhat-chai-matchers": ^1.0.3 + "@nomicfoundation/hardhat-toolbox": ^1.0.2 + "@nomiclabs/hardhat-ethers": ^2.0.5 + "@nomiclabs/hardhat-waffle": ^2.0.2 + "@openzeppelin/contracts-upgradeable": ^4.6.0 + "@trivago/prettier-plugin-sort-imports": ^3.2.0 + "@typechain/ethers-v5": 10.0.0 + "@typechain/hardhat": ^6.0.0 + "@types/mocha": ^9.1.0 + "@typescript-eslint/eslint-plugin": ^5.27.0 + "@typescript-eslint/parser": ^5.27.0 + chai: ^4.3.0 + eslint: ^8.16.0 + eslint-config-prettier: ^8.5.0 + ethereum-waffle: ^3.4.4 + ethers: ^5.6.8 + hardhat: ^2.8.4 + hardhat-gas-reporter: ^1.0.7 + prettier: ^2.4.1 + prettier-plugin-solidity: ^1.0.0-beta.5 + solhint: ^3.3.2 + solhint-plugin-prettier: ^0.0.5 + solidity-coverage: ^0.7.14 + ts-node: ^10.8.0 + typechain: 8.0.0 + typescript: ^4.7.2 + languageName: unknown + linkType: soft + "@abacus-network/monorepo@workspace:.": version: 0.0.0-use.local resolution: "@abacus-network/monorepo@workspace:." @@ -4088,6 +4124,52 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/hardhat-chai-matchers@npm:^1.0.3": + version: 1.0.3 + resolution: "@nomicfoundation/hardhat-chai-matchers@npm:1.0.3" + dependencies: + "@ethersproject/abi": ^5.1.2 + "@types/chai-as-promised": ^7.1.3 + chai-as-promised: ^7.1.1 + chalk: ^2.4.2 + deep-eql: ^4.0.1 + ordinal: ^1.0.3 + peerDependencies: + "@nomiclabs/hardhat-ethers": ^2.0.0 + chai: ^4.2.0 + ethers: ^5.0.0 + hardhat: ^2.9.4 + checksum: bee445e7ed53d6ee00b85c5c76ca79b7cd1792487481803a29357803b09f92b626b6429a50522a8398c8010ad4fd1f0106c79416dc8c01aea644e7360ea9c203 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-toolbox@npm:^1.0.2": + version: 1.0.2 + resolution: "@nomicfoundation/hardhat-toolbox@npm:1.0.2" + peerDependencies: + "@ethersproject/abi": ^5.4.7 + "@ethersproject/providers": ^5.4.7 + "@nomicfoundation/hardhat-chai-matchers": ^1.0.0 + "@nomicfoundation/hardhat-network-helpers": ^1.0.0 + "@nomiclabs/hardhat-ethers": ^2.0.0 + "@nomiclabs/hardhat-etherscan": ^3.0.0 + "@typechain/ethers-v5": ^10.1.0 + "@typechain/hardhat": ^6.1.2 + "@types/chai": ^4.2.0 + "@types/mocha": ^9.1.0 + "@types/node": ">=12.0.0" + chai: ^4.2.0 + ethers: ^5.4.7 + hardhat: ^2.9.9 + hardhat-gas-reporter: ^1.0.8 + solidity-coverage: ^0.7.21 + ts-node: ">=8.0.0" + typechain: ^8.1.0 + typescript: ">=4.5.0" + checksum: d13b3e9f08d8be5f72a25872b6a9d3609fdb34f46b47dbf10963a6fb5003ac1cd0d0c107ef6d91807b864a8766d9e5092518f21db3116902ee5a8ae73fdcaaa2 + languageName: node + linkType: hard + "@nomiclabs/hardhat-ethers@npm:^2.0.5": version: 2.0.6 resolution: "@nomiclabs/hardhat-ethers@npm:2.0.6" @@ -4542,6 +4624,15 @@ __metadata: languageName: node linkType: hard +"@types/chai-as-promised@npm:^7.1.3": + version: 7.1.5 + resolution: "@types/chai-as-promised@npm:7.1.5" + dependencies: + "@types/chai": "*" + checksum: 7c1345c6e32513d52d8e562ec173c23161648d6b792046525f18803a9932d7b3ad3dca8f0181e3c529ec42b106099f174e34edeb184d61dc93e32c98b5132fd4 + languageName: node + linkType: hard + "@types/chai@npm:*, @types/chai@npm:^4.2.21": version: 4.3.1 resolution: "@types/chai@npm:4.3.1" @@ -6855,6 +6946,17 @@ __metadata: languageName: node linkType: hard +"chai-as-promised@npm:^7.1.1": + version: 7.1.1 + resolution: "chai-as-promised@npm:7.1.1" + dependencies: + check-error: ^1.0.2 + peerDependencies: + chai: ">= 2.1.2 < 5" + checksum: 7262868a5b51a12af4e432838ddf97a893109266a505808e1868ba63a12de7ee1166e9d43b5c501a190c377c1b11ecb9ff8e093c89f097ad96c397e8ec0f8d6a + languageName: node + linkType: hard + "chai@npm:^4.3.0, chai@npm:^4.3.4, chai@npm:^4.3.6": version: 4.3.6 resolution: "chai@npm:4.3.6" @@ -7704,6 +7806,15 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^4.0.1": + version: 4.1.0 + resolution: "deep-eql@npm:4.1.0" + dependencies: + type-detect: ^4.0.0 + checksum: 2fccd527df9a70a92a1dfa8c771d139753625938e137b09fc946af8577d22360ef28d3c74f0e9c5aaa399bab20542d0899da1529c71db76f280a30147cd2a110 + languageName: node + linkType: hard + "deep-equal@npm:~1.1.1": version: 1.1.1 resolution: "deep-equal@npm:1.1.1" @@ -13612,6 +13723,13 @@ __metadata: languageName: node linkType: hard +"ordinal@npm:^1.0.3": + version: 1.0.3 + resolution: "ordinal@npm:1.0.3" + checksum: 6761c5b7606b6c4b0c22b4097dab4fe7ffcddacc49238eedf9c0ced877f5d4e4ad3f4fd43fefa1cc3f167cc54c7149267441b2ae85b81ccf13f45cf4b7947164 + languageName: node + linkType: hard + "os-homedir@npm:^1.0.0": version: 1.0.2 resolution: "os-homedir@npm:1.0.2"