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