Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement proxy contract #66

Merged
merged 29 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3e52f35
Implement proxy contract
karim-en Apr 9, 2024
a404a79
Add check for previous events
karim-en Apr 9, 2024
882c7ab
Add second solution
karim-en Apr 9, 2024
4521dd0
Revert naming changes from 331c8bf40e969c0659e79449d7c5dd06f537ac65 t…
kiseln Apr 17, 2024
c07183f
Remove second solution for proxy contract. Add necessary functions
kiseln Apr 17, 2024
8b7be73
Add tests for eth custodian proxy
kiseln Apr 18, 2024
f17bc73
Remove leftover variables
kiseln Apr 19, 2024
898d2fc
Remove unused code
kiseln Apr 22, 2024
29b2706
Convert SelectivePausable contract to namespaced storage (eip-7201)
kiseln Apr 22, 2024
4c5b874
Combine withdraw functionality into a single function
kiseln Apr 22, 2024
ec064b5
Add generic admin method on the proxy
kiseln Apr 29, 2024
16da39b
Correct deployment script
kiseln May 6, 2024
c457ab1
Test corrections
kiseln May 9, 2024
b0dd634
Update constant
kiseln May 15, 2024
6b352cf
Fixed event name in tests
kiseln May 15, 2024
ecc9f26
update config
olga24912 May 24, 2024
1ee51e8
add etherscan verififcation
olga24912 Jun 5, 2024
2c30d2c
update admin scripts
olga24912 Jun 5, 2024
ee19546
PAUSED_WITHDRAW -> PAUSED_WITHDRAW_POST_MIGRATION
olga24912 Jun 10, 2024
41ec3a6
add fee for backword compatibility
olga24912 Jun 24, 2024
e810fdc
emit Deposited event in Proxy Contract
olga24912 Jun 26, 2024
d9d860f
Add gap for future implementation of ProofKeeper for the proxy
kiseln Jul 2, 2024
40f67a9
Update gap value
kiseln Jul 2, 2024
c5a28bc
Add `updateAdminLegacy` script
karim-en Jul 3, 2024
acb3076
Fix verification
karim-en Jul 3, 2024
eef0f33
Update hardhat and ethers to v6
karim-en Jul 3, 2024
3f3f420
Fix test
karim-en Jul 3, 2024
02d3f22
Extract block height from proof (#67)
olga24912 Aug 3, 2024
d44af96
Add upgrade script
karim-en Aug 5, 2024
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ After that call: <br />
As a result of the function call you will get the address of the freshly deployed `EthCustodian` that you can put in
your `ethereum-config.json` file in the `ethConnectorAddress` field.

After `ethConnectorAddress` is set, you can run

`$ make eth-deploy-proxy`

to deploy the proxy contract and make it the admin of `EthCustodian`.

### Other scripts

For more advanced usage, please examine the `hardhat.config.js` file which contains a lot of scripts that are performed
Expand Down
5 changes: 4 additions & 1 deletion eth-custodian/Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: compile test

NETWORK=ropsten
NETWORK=sepolia
NEAR_NETWORK=testnet
NEAR_EVM_ACCOUNT=aurora
NEAR_ACCOUNT=
Expand Down Expand Up @@ -38,6 +38,9 @@ eth-get-prover-address:
eth-deploy-contracts:
yarn hardhat run --network ${NETWORK} scripts/eth_deploy_contracts.js

eth-deploy-proxy:
yarn hardhat run --network ${NETWORK} scripts/eth_deploy_proxy.js

eth-deposit-to-near:
yarn hardhat --network ${NETWORK} eth-deposit-to-near --near-recipient ${NEAR_RECIPIENT} --amount ${AMOUNT} --fee ${FEE}

Expand Down
2 changes: 1 addition & 1 deletion eth-custodian/contracts/EthCustodian.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ contract EthCustodian is ProofKeeper, AdminControlled {

string memory protocolMessage = string(
abi.encodePacked(
string(nearProofProducerAccount),
string(nearProofProducerAccount_),
MESSAGE_SEPARATOR, ethRecipientOnNear
)
);
Expand Down
133 changes: 133 additions & 0 deletions eth-custodian/contracts/EthCustodianProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8;
import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol';
import {EthCustodian} from './EthCustodian.sol';
import {SelectivePausableUpgradable} from './SelectivePausableUpgradable.sol';

contract EthCustodianProxy is
karim-en marked this conversation as resolved.
Show resolved Hide resolved
Initializable,
UUPSUpgradeable,
AccessControlUpgradeable,
SelectivePausableUpgradable
{
olga24912 marked this conversation as resolved.
Show resolved Hide resolved
bytes32 public constant PAUSABLE_ADMIN_ROLE = keccak256('PAUSABLE_ADMIN_ROLE');

uint constant UNPAUSED_ALL = 0;
uint constant PAUSED_DEPOSIT_TO_EVM = 1 << 0;
uint constant PAUSED_DEPOSIT_TO_NEAR = 1 << 1;
uint constant PAUSED_WITHDRAW_POST_MIGRATION = 1 << 2;
uint constant PAUSED_WITHDRAW_PRE_MIGRATION = 1 << 3;

error AlreadyMigrated();
error ProducerAccountIdTooLong(bytes newProducerAccount);
error ProofFromPostMergeBlock();

bytes public preMigrationProducerAccount;
uint64 public migrationBlockHeight;

EthCustodian public ethCustodianImpl;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(
EthCustodian _ethCustodianImpl
) public initializer {
ethCustodianImpl = _ethCustodianImpl;
__Pausable_init();
__AccessControl_init();
__UUPSUpgradeable_init();
_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
_grantRole(PAUSABLE_ADMIN_ROLE, _msgSender());
}

function depositToNear(
string memory nearRecipientAccountId
) external payable whenNotPaused(PAUSED_DEPOSIT_TO_NEAR) {
olga24912 marked this conversation as resolved.
Show resolved Hide resolved
olga24912 marked this conversation as resolved.
Show resolved Hide resolved
ethCustodianImpl.depositToNear{value: msg.value}(nearRecipientAccountId, 0);
}

function depositToEVM(
string memory ethRecipientOnNear
) external payable whenNotPaused(PAUSED_DEPOSIT_TO_EVM) {
ethCustodianImpl.depositToEVM{value: msg.value}(ethRecipientOnNear, 0);
}

function withdraw(
bytes calldata proofData,
uint64 proofBlockHeight
) external {
if (proofBlockHeight > migrationBlockHeight) {
_requireNotPaused(PAUSED_WITHDRAW_POST_MIGRATION);
ethCustodianImpl.withdraw(proofData, proofBlockHeight);
} else {
_requireNotPaused(PAUSED_WITHDRAW_PRE_MIGRATION);
bytes memory postMigrationProducer = ethCustodianImpl.nearProofProducerAccount_();
_writeProofProducerSlot(preMigrationProducerAccount);
ethCustodianImpl.withdraw(proofData, proofBlockHeight);
_writeProofProducerSlot(postMigrationProducer);
}
}

function migrateToNewProofProducer(
bytes calldata newProducerAccount,
uint64 migrationBlockNumber
) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (keccak256(preMigrationProducerAccount) != keccak256(hex"")) {
revert AlreadyMigrated();
}

// Needs to fit in one slot
if (newProducerAccount.length > 31) {
revert ProducerAccountIdTooLong(newProducerAccount);
}

migrationBlockHeight = migrationBlockNumber;
preMigrationProducerAccount = ethCustodianImpl.nearProofProducerAccount_();
_writeProofProducerSlot(newProducerAccount);
}

function callImpl(bytes calldata data) external payable onlyRole(DEFAULT_ADMIN_ROLE) {
(bool success, ) = address(ethCustodianImpl).call{value: msg.value}(data);
require(success, 'EthCustodian call failed');
}

function pauseAll() external onlyRole(PAUSABLE_ADMIN_ROLE) {
uint flags = PAUSED_DEPOSIT_TO_EVM |
PAUSED_DEPOSIT_TO_NEAR |
PAUSED_WITHDRAW_POST_MIGRATION |
PAUSED_WITHDRAW_PRE_MIGRATION;
ethCustodianImpl.adminPause(flags);
_pause(flags);
}

function pauseImpl(uint flags) external onlyRole(DEFAULT_ADMIN_ROLE) {
ethCustodianImpl.adminPause(flags);
}

function pauseProxy(uint flags) external onlyRole(DEFAULT_ADMIN_ROLE) {
_pause(flags);
}

/**
* @dev Internal function called by the proxy contract to authorize an upgrade to a new implementation address
* using the UUPS proxy upgrade pattern. Overrides the default `_authorizeUpgrade` function from the `UUPSUpgradeable` contract.
* This function does not need to perform any extra authorization checks other than restricting the execution of the function to the admin and reverting otherwise.
* @param newImplementation Address of the new implementation contract.
* Requirements:
* - The caller must have the `DEFAULT_ADMIN_ROLE`.
*/
function _authorizeUpgrade(
address newImplementation
) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}

function _writeProofProducerSlot(bytes memory proofProducer) private {
uint dataLength = proofProducer.length * 2;
ethCustodianImpl.adminSstore(1, uint(bytes32(proofProducer)) + dataLength);
}
}
24 changes: 12 additions & 12 deletions eth-custodian/contracts/ProofKeeper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ contract ProofKeeper {
using Borsh for Borsh.Data;
using ProofDecoder for Borsh.Data;

INearProver public prover;
bytes public nearProofProducerAccount;
INearProver public prover_;
bytes public nearProofProducerAccount_;

/// Proofs from blocks that are below the acceptance height will be rejected.
// If `minBlockAcceptanceHeight` value is zero - proofs from block with any height are accepted.
uint64 public minBlockAcceptanceHeight;
uint64 public minBlockAcceptanceHeight_;

// OutcomeReciptId -> Used
mapping(bytes32 => bool) public usedEvents;
mapping(bytes32 => bool) public usedEvents_;

constructor(
bytes memory _nearProofProducerAccount,
Expand All @@ -34,9 +34,9 @@ contract ProofKeeper {
'Invalid Near prover address'
);

nearProofProducerAccount = _nearProofProducerAccount;
prover = _prover;
minBlockAcceptanceHeight = _minBlockAcceptanceHeight;
nearProofProducerAccount_ = _nearProofProducerAccount;
prover_ = _prover;
minBlockAcceptanceHeight_ = _minBlockAcceptanceHeight;
}

/// Parses the provided proof and consumes it if it's not already used.
Expand All @@ -49,11 +49,11 @@ contract ProofKeeper {
returns(ProofDecoder.ExecutionStatus memory result)
{
require(
proofBlockHeight >= minBlockAcceptanceHeight,
proofBlockHeight >= minBlockAcceptanceHeight_,
'Proof is from an ancient block'
);
require(
prover.proveOutcome(proofData,proofBlockHeight),
prover_.proveOutcome(proofData,proofBlockHeight),
'Proof should be valid'
);

Expand All @@ -66,14 +66,14 @@ contract ProofKeeper {
bytes32 receiptId = fullOutcomeProof.outcome_proof.outcome_with_id.outcome.receipt_ids[0];

require(
!usedEvents[receiptId],
!usedEvents_[receiptId],
'The burn event cannot be reused'
);
usedEvents[receiptId] = true;
usedEvents_[receiptId] = true;

require(
keccak256(fullOutcomeProof.outcome_proof.outcome_with_id.outcome.executor_id) ==
keccak256(nearProofProducerAccount),
keccak256(nearProofProducerAccount_),
'Can only withdraw coins from the linked proof producer on Near blockchain'
);

Expand Down
113 changes: 113 additions & 0 deletions eth-custodian/contracts/SelectivePausableUpgradable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8;

import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

/**
* @dev Contract module which allows children to implement an emergency stop
* mechanism that can be triggered by an authorized account.
*
* This module is used through inheritance. It will make available the
* modifiers `whenNotPaused` and `whenPaused`, which can be applied to
* the functions of your contract. Note that they will not be pausable by
* simply including this module, only once the modifiers are put in place.
*/
abstract contract SelectivePausableUpgradable is Initializable, ContextUpgradeable {

struct SelectivePausableStorage {
uint _pausedFlags;
}

// keccak256(abi.encode(uint256(keccak256("aurora.SelectivePausable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant SelectivePausableStorageLocation = 0x3385e98de875c27690676838324244576ee92c4384629b3b7dd9c0a7c978e200;

function _getSelectivePausableStorage() private pure returns (SelectivePausableStorage storage $) {
assembly {
$.slot := SelectivePausableStorageLocation
}
}

/**
* @dev Emitted when the pause is triggered by `account`.
*/
event Paused(address account, uint flags);

uint private _pausedFlags;

/**
* @dev Initializes the contract in unpaused state.
*/
function __Pausable_init() internal onlyInitializing {
__Pausable_init_unchained();
}

function __Pausable_init_unchained() internal onlyInitializing {
SelectivePausableStorage storage $ = _getSelectivePausableStorage();
$._pausedFlags = 0;
}

/**
* @dev Modifier to make a function callable only when the contract is not paused.
*
* Requirements:
*
* - The contract must not be paused.
*/
modifier whenNotPaused(uint flag) {
_requireNotPaused(flag);
_;
}

/**
* @dev Modifier to make a function callable only when the contract is paused.
*
* Requirements:
*
* - The contract must be paused.
*/
modifier whenPaused(uint flag) {
_requirePaused(flag);
_;
}

/**
* @dev Returns true if the contract is paused, and false otherwise.
*/
function paused(uint flag) public view virtual returns (bool) {
SelectivePausableStorage storage $ = _getSelectivePausableStorage();
return ($._pausedFlags & flag) != 0;
}

/**
* @dev Returns paused flags.
*/
function pausedFlags() public view virtual returns (uint) {
SelectivePausableStorage storage $ = _getSelectivePausableStorage();
return $._pausedFlags;
}

/**
* @dev Throws if the contract is paused.
*/
function _requireNotPaused(uint flag) internal view virtual {
require(!paused(flag), "Pausable: paused");
}

/**
* @dev Throws if the contract is not paused.
*/
function _requirePaused(uint flag) internal view virtual {
require(paused(flag), "Pausable: not paused");
}

/**
* @dev Triggers stopped state.
*/
function _pause(uint flags) internal virtual {
SelectivePausableStorage storage $ = _getSelectivePausableStorage();
$._pausedFlags = flags;
emit Paused(_msgSender(), $._pausedFlags);
}
}
13 changes: 7 additions & 6 deletions eth-custodian/hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
require('dotenv').config();
require('@nomiclabs/hardhat-waffle');
require('hardhat-gas-reporter');
require('@openzeppelin/hardhat-upgrades');

const ROPSTEN_WEB3_RPC_ENDPOINT = process.env.ROPSTEN_WEB3_RPC_ENDPOINT;
const GOERLI_WEB3_RPC_ENDPOINT = process.env.GOERLI_WEB3_RPC_ENDPOINT;
const SEPOLIA_WEB3_RPC_ENDPOINT = process.env.SEPOLIA_WEB3_RPC_ENDPOINT;
const MAINNET_WEB3_RPC_ENDPOINT = process.env.MAINNET_WEB3_RPC_ENDPOINT;
const AURORA_WEB3_RPC_ENDPOINT = process.env.AURORA_WEB3_RPC_ENDPOINT;
// Hardhat workaround to specify some random private key so this won't fail in CI
const ROPSTEN_PRIVATE_KEY = process.env.ROPSTEN_PRIVATE_KEY ? process.env.ROPSTEN_PRIVATE_KEY : "00".repeat(32);
const GOERLI_PRIVATE_KEY = process.env.GOERLI_PRIVATE_KEY ? process.env.GOERLI_PRIVATE_KEY : "00".repeat(32);
const SEPOLIA_PRIVATE_KEY = process.env.SEPOLIA_PRIVATE_KEY ? process.env.SEPOLIA_PRIVATE_KEY : "00".repeat(32);
const MAINNET_PRIVATE_KEY = process.env.MAINNET_PRIVATE_KEY ? process.env.MAINNET_PRIVATE_KEY : "00".repeat(32);
const AURORA_PRIVATE_KEY = process.env.AURORA_PRIVATE_KEY ? process.env.AURORA_PRIVATE_KEY : "00".repeat(32);

Expand Down Expand Up @@ -264,7 +265,7 @@ module.exports = {
solidity: {
compilers: [
{
version: "0.8.11",
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
Expand All @@ -279,9 +280,9 @@ module.exports = {
url: `${ROPSTEN_WEB3_RPC_ENDPOINT}`,
accounts: [`0x${ROPSTEN_PRIVATE_KEY}`]
},
goerli: {
url: `${GOERLI_WEB3_RPC_ENDPOINT}`,
accounts: [`0x${GOERLI_PRIVATE_KEY}`]
sepolia: {
url: `${SEPOLIA_WEB3_RPC_ENDPOINT}`,
accounts: [`0x${SEPOLIA_PRIVATE_KEY}`]
},
mainnet: {
url: `${MAINNET_WEB3_RPC_ENDPOINT}`,
Expand Down
Loading
Loading