Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/evm/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ AXIA=0x3F4C47E37A94caeE31d0B585f54F3fFA1f2294C9
SOLVER=0xE0D76433Edd9f5df370561bd0AF231E72c83Cd3a
VALIDATOR=0xc76B16fA2Fa75D93e08099DC16413D9a083404A1

SETTLER_PROXY=

ETHERSCAN_KEY=
DEPLOYER_PRIVATE_KEY=
34 changes: 22 additions & 12 deletions packages/evm/contracts/Settler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@

pragma solidity ^0.8.20;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
import '@openzeppelin/contracts/utils/cryptography/EIP712.sol';
import '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol';

import './Intents.sol';
import './dynamic-calls/DynamicCallEncoder.sol';
import './interfaces/IController.sol';
import './interfaces/IDynamicCallEncoder.sol';
import './interfaces/IOperationsValidator.sol';
Expand All @@ -38,16 +37,15 @@ import './smart-accounts/SmartAccountsHandlerHelpers.sol';
* @title Settler
* @dev Contract that provides the appropriate context for solvers to execute proposals that fulfill user intents
*/
contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
contract Settler is ISettler, Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable, EIP712Upgradeable {
using SafeERC20 for IERC20;
using IntentsHelpers for Intent;
using IntentsHelpers for Proposal;
using IntentsHelpers for Validation;
using SmartAccountsHandlerHelpers for address;

// Mimic controller reference
// solhint-disable-next-line immutable-vars-naming
address public immutable override controller;
address public override controller;

// Smart accounts handler reference
address public override smartAccountsHandler;
Expand All @@ -74,14 +72,26 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
}

/**
* @dev Creates a new Settler contract
* @dev Disables initializers to prevent implementation contract from being initialized directly
*/
constructor() {
_disableInitializers();
}

/**
* @dev Initializes a new Settler contract
* @param _controller Address of the Settler controller
* @param _owner Address that will own the contract
* @param _dynamicCallEncoder Address of the dynamic call encoder
*/
constructor(address _controller, address _owner) Ownable(_owner) EIP712('Mimic Protocol Settler', '1') {
function initialize(address _controller, address _owner, address _dynamicCallEncoder) external initializer {
__Ownable_init(_owner);
__ReentrancyGuard_init();
__EIP712_init('Mimic Protocol Settler', '1');

controller = _controller;
smartAccountsHandler = address(new SmartAccountsHandler());
dynamicCallEncoder = address(new DynamicCallEncoder());
_setDynamicCallEncoder(_dynamicCallEncoder);
Comment on lines -84 to +94
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to remove DynamicCallEncoder creation to avoid "contract size too big" error

}

/**
Expand Down Expand Up @@ -299,7 +309,7 @@ contract Settler is ISettler, Ownable, ReentrancyGuard, EIP712 {
* @dev Validates and executes a proposal to fulfill a transfer operation
* @param intent Intent that contains transfer operation to be fulfilled
* @param proposal Transfer proposal to be executed
* @param index Position where the trasnfer proposal data and operation are located
* @param index Position where the transfer proposal data and operation are located
*/
function _executeTransfer(Intent memory intent, Proposal memory proposal, uint256 index)
internal
Expand Down
23 changes: 23 additions & 0 deletions packages/evm/contracts/proxy/Proxy.sol
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use this to compile npm files directly instead of doing this. You will need to upgrade hardhat to the latest version
https://hardhat.org/docs/cookbook/npm-artifacts

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't make it work. It worked for deploying the contract, but failed when trying to verify it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.8.20;

import '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol';

contract Proxy is TransparentUpgradeableProxy {
constructor(address implementation, address initialOwner, bytes memory data)
TransparentUpgradeableProxy(implementation, initialOwner, data)
{}
}
23 changes: 23 additions & 0 deletions packages/evm/contracts/test/SettlerV2Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.8.20;

import '../Settler.sol';

contract SettlerV2Mock is Settler {
function someNewFunction() external pure returns (string memory) {
return 'Some new function';
}
}
1 change: 1 addition & 0 deletions packages/evm/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dotenv.config()
const config: HardhatUserConfig = {
plugins: [hardhatVerify, hardhatToolboxMochaEthersPlugin],
solidity: {
dependenciesToCompile: ['@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol'],
profiles: {
default: {
version: '0.8.28',
Expand Down
3 changes: 2 additions & 1 deletion packages/evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"test": "hardhat test"
},
"dependencies": {
"@openzeppelin/contracts": "5.3.0"
"@openzeppelin/contracts": "5.3.0",
"@openzeppelin/contracts-upgradeable": "5.3.0"
},
"devDependencies": {
"@mimicprotocol/sdk": "~0.1.0",
Expand Down
18 changes: 16 additions & 2 deletions packages/evm/scripts/deploy-contracts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Interface } from 'ethers'

import ControllerArtifact from '../artifacts/contracts/Controller.sol/Controller.json'
import DynamicCallEncoderArtifact from '../artifacts/contracts/dynamic-calls/DynamicCallEncoder.sol/DynamicCallEncoder.json'
import ProxyArtifact from '../artifacts/contracts/proxy/Proxy.sol/Proxy.json'
import SettlerArtifact from '../artifacts/contracts/Settler.sol/Settler.json'
import SmartAccount7702 from '../artifacts/contracts/smart-accounts/SmartAccount7702.sol/SmartAccount7702.json'
import MimicHelperArtifact from '../artifacts/contracts/utils/MimicHelper.sol/MimicHelper.json'
Expand All @@ -15,8 +19,18 @@ async function main(): Promise<void> {

const controllerArgs = [ADMIN, [SOLVER], [], [AXIA], [VALIDATOR], MIN_VALIDATORS]
const controller = await deployCreate3(ControllerArtifact, controllerArgs, '0x17')
const settler = await deployCreate3(SettlerArtifact, [controller.target, ADMIN], '0x18')
await deployCreate3(SmartAccount7702, [settler.target], '0x19')

const dynamicCallEncoder = await deployCreate3(DynamicCallEncoderArtifact, [], '0x20')
const settlerImplementation = await deployCreate3(SettlerArtifact, [], '0x1801')

const initializeData = new Interface(SettlerArtifact.abi).encodeFunctionData('initialize', [
controller.target,
ADMIN,
dynamicCallEncoder.target,
])
const settlerProxy = await deployCreate3(ProxyArtifact, [settlerImplementation.target, ADMIN, initializeData], '0x18')

await deployCreate3(SmartAccount7702, [settlerProxy.target], '0x19')
await deployCreate3(MimicHelperArtifact, [], '0x42')
}

Expand Down
36 changes: 36 additions & 0 deletions packages/evm/scripts/upgrade-settler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { HardhatEthers, HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'
import { Contract, getAddress } from 'ethers'
import { network } from 'hardhat'

import ProxyAdminArtifact from '../artifacts/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol/ProxyAdmin.json'
import SettlerArtifact from '../artifacts/contracts/Settler.sol/Settler.json'
import { deployCreate3 } from './deploy-create3'

const ERC1967_ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'

async function main(): Promise<void> {
const { ethers } = await network.connect()
const [signer] = await ethers.getSigners()

if (!process.env.SETTLER_PROXY) throw Error('SETTLER_PROXY env variable not provided')
const proxy = getAddress(process.env.SETTLER_PROXY)

const proxyAdmin = await getProxyAdmin(ethers, proxy, signer)
const proxyAdminOwner = await proxyAdmin.owner()
if (proxyAdminOwner !== signer.address) {
throw Error(`Signer ${signer.address} is not the ProxyAdmin owner ${proxyAdminOwner}`)
}

const implementation = await deployCreate3(SettlerArtifact, [], '0x1802')
const tx = await proxyAdmin.upgradeAndCall(proxy, implementation.target, '0x')
await tx.wait()
console.log(`✅ Settler ${proxy} upgraded in tx ${tx.hash}`)
}

async function getProxyAdmin(ethers: HardhatEthers, proxy: string, signer: HardhatEthersSigner): Promise<Contract> {
const rawAdmin = await ethers.provider.getStorage(proxy, ERC1967_ADMIN_SLOT)
const adminAddress = getAddress(`0x${rawAdmin.slice(-40)}`)
return ethers.getContractAt(ProxyAdminArtifact.abi, adminAddress, signer)
}

main().catch(console.error)
47 changes: 36 additions & 11 deletions packages/evm/test/Settler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
TransferExecutorMock,
} from '../types/ethers-contracts/index.js'
import itBehavesLikeOwnable from './behaviors/Ownable.behavior'
import itBehavesLikeUpgradeable from './behaviors/Upgradeable.behavior'
import {
Account,
CallOperation,
Expand All @@ -52,6 +53,7 @@ import {
createTransferOperation,
createTransferProposal,
currentTimestamp,
deployProxy,
DynamicCallOperation,
hashIntent,
hashProposal,
Expand All @@ -75,15 +77,20 @@ const { ethers } = await network.connect()
/* eslint-disable @typescript-eslint/no-non-null-assertion */

describe('Settler', () => {
let settler: Settler, controller: Controller
let user: HardhatEthersSigner, other: HardhatEthersSigner
let admin: HardhatEthersSigner, owner: HardhatEthersSigner, solver: HardhatEthersSigner
let settler: Settler, controller: Controller, dynamicCallEncoder: DynamicCallEncoder
let user: HardhatEthersSigner, other: HardhatEthersSigner, solver: HardhatEthersSigner
let admin: HardhatEthersSigner, owner: HardhatEthersSigner, proxyOwner: HardhatEthersSigner

beforeEach('deploy settler', async () => {
// eslint-disable-next-line prettier/prettier
[, admin, owner, user, other, solver] = await ethers.getSigners()
[, admin, owner, user, other, solver, proxyOwner] = await ethers.getSigners()
controller = await ethers.deployContract('Controller', [admin, [], [], [], [], 0])
settler = await ethers.deployContract('Settler', [controller, owner])
dynamicCallEncoder = await ethers.deployContract('DynamicCallEncoder', [])
settler = await deployProxy<Settler>(ethers, 'Settler', proxyOwner, [
controller.target,
owner.address,
dynamicCallEncoder.target,
])
})

const balanceOf = (token: TokenMock | string, account: Account) => {
Expand All @@ -107,10 +114,33 @@ describe('Settler', () => {
})

it('has a dynamic call decoder', async () => {
expect(await settler.dynamicCallEncoder()).to.not.be.equal(ZERO_ADDRESS)
expect(await settler.dynamicCallEncoder()).to.be.equal(dynamicCallEncoder)
})
})

describe('upgradeable', () => {
beforeEach('set upgradeable context', function () {
this.ethers = ethers
this.proxy = settler
this.proxyOwner = proxyOwner
this.other = other
this.implementationNameV1 = 'Settler'
this.implementationNameV2 = 'SettlerV2Mock'
this.initializeArgs = [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS]
this.assertUpgrade = async (proxy: Settler) => {
const upgraded = await ethers.getContractAt('SettlerV2Mock', proxy)
expect(await upgraded.someNewFunction()).to.be.equal('Some new function')
expect(await upgraded.owner()).to.be.equal(owner)
expect(await upgraded.controller()).to.be.equal(controller)
expect(await upgraded.operationsValidator()).to.be.equal(ZERO_ADDRESS)
expect(await upgraded.smartAccountsHandler()).to.not.be.equal(ZERO_ADDRESS)
expect(await upgraded.dynamicCallEncoder()).to.be.equal(dynamicCallEncoder)
}
})

itBehavesLikeUpgradeable()
})

describe('ownable', () => {
beforeEach('set instance', function () {
this.owner = owner
Expand Down Expand Up @@ -3081,7 +3111,6 @@ describe('Settler', () => {
let target: Account
let feeToken: TokenMock
let proposal: Proposal
let dynamicCallEncoder: DynamicCallEncoder

const arg0 = randomEvmAddress()
const arg1 = randomNumber(2)
Expand Down Expand Up @@ -3136,10 +3165,6 @@ describe('Settler', () => {
proposal = createDynamicCallProposal({ fees: [feeAmount] })
})

beforeEach('set dynamic call encoder', async () => {
dynamicCallEncoder = await ethers.deployContract('DynamicCallEncoder', [])
})

it('executes the intent', async () => {
const preUserBalance = await balanceOf(feeToken, user)
const preSolverBalance = await balanceOf(feeToken, solver)
Expand Down
62 changes: 62 additions & 0 deletions packages/evm/test/behaviors/Upgradeable.behavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { HardhatEthers } from '@nomicfoundation/hardhat-ethers/types'
import { expect } from 'chai'
import { Contract, getAddress } from 'ethers'

/* eslint-disable no-secrets/no-secrets */

const ERC1967_ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'

export default function itBehavesLikeUpgradeable(): void {
describe('initialize', () => {
it('locks the implementation initializer', async function () {
const implementation = await this.ethers.deployContract(this.implementationNameV1)

await expect(implementation.initialize(...this.initializeArgs)).to.be.revertedWithCustomError(
implementation,
'InvalidInitialization'
)
})

it('cannot be initialized twice', async function () {
await expect(this.proxy.initialize(...this.initializeArgs)).to.be.revertedWithCustomError(
this.proxy,
'InvalidInitialization'
)
})
})

describe('upgradeAndCall', () => {
context('when the sender is the owner', () => {
it('upgrades the implementation', async function () {
const proxyAdmin = await getProxyAdmin(this.ethers, this.proxy)
const newImplementation = await this.ethers.deployContract(this.implementationNameV2)

await proxyAdmin.connect(this.proxyOwner).upgradeAndCall(this.proxy, newImplementation, '0x')
await this.assertUpgrade(this.proxy)
})
})

context('when the sender is not the owner', () => {
it('reverts', async function () {
const proxyAdmin = await getProxyAdmin(this.ethers, this.proxy)
const newImplementation = await this.ethers.deployContract(this.implementationNameV2)

await expect(
proxyAdmin.connect(this.other).upgradeAndCall(this.proxy, newImplementation, '0x')
).to.be.revertedWithCustomError(proxyAdmin, 'OwnableUnauthorizedAccount')
})
})
})
}

async function getProxyAdmin(ethers: HardhatEthers, proxy: Contract): Promise<Contract> {
const rawAdmin = await ethers.provider.getStorage(proxy.target as string, ERC1967_ADMIN_SLOT)
return new Contract(
getAddress(`0x${rawAdmin.slice(-40)}`),
[
'error OwnableUnauthorizedAccount(address account)',
'function upgradeAndCall(address proxy, address implementation, bytes data) payable',
],
ethers.provider
)
}
1 change: 1 addition & 0 deletions packages/evm/test/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './arrays'
export * from './dynamic-calls.js'
export * from './intents'
export * from './proposal'
export * from './proxy'
export * from './safeguards'
export * from './time'
Loading
Loading