Add interchain accounts package (#1057)
* Push initial ICA package * Update README * Remove generated files * Make _salt pure * Rename package to interchain accountsnambrot/verification-fies
parent
24e2c81236
commit
53593ef6f3
@ -0,0 +1,9 @@ |
||||
artifacts/ |
||||
cache/ |
||||
coverage/ |
||||
coverage.json |
||||
dist/ |
||||
node_modules/ |
||||
types/ |
||||
*.swp |
||||
.yarn/install-state.gz |
@ -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. |
@ -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); |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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?
|
||||
}, |
||||
}; |
@ -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" |
||||
} |
@ -0,0 +1,16 @@ |
||||
import { RouterContracts, RouterFactories } from '@abacus-network/sdk'; |
||||
|
||||
import { |
||||
InterchainAccountRouter, |
||||
InterchainAccountRouter__factory, |
||||
} from '../types'; |
||||
|
||||
export type InterchainAccountFactories = |
||||
RouterFactories<InterchainAccountRouter>; |
||||
|
||||
export const InterchainAccountFactories: InterchainAccountFactories = { |
||||
router: new InterchainAccountRouter__factory(), |
||||
}; |
||||
|
||||
export type InterchainAccountContracts = |
||||
RouterContracts<InterchainAccountRouter>; |
@ -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<Chain>, |
||||
configMap: ChainMap<Chain, InterchainAccountConfig>, |
||||
protected core: AbacusCore<Chain>, |
||||
) { |
||||
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, |
||||
}; |
||||
} |
||||
} |
@ -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<TestChainNames>; |
||||
let coreApp: TestCoreApp; |
||||
let config: ChainMap<TestChainNames, RouterConfig>; |
||||
|
||||
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); |
||||
}); |
||||
}); |
@ -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"] |
||||
} |
@ -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); |
Loading…
Reference in new issue