Skip to content
This repository has been archived by the owner on May 9, 2024. It is now read-only.

Commit

Permalink
Implement EIP712 for signing (#628)
Browse files Browse the repository at this point in the history
  • Loading branch information
nmlinaric authored Sep 2, 2022
1 parent 076fc37 commit fe8628b
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 231 deletions.
76 changes: 50 additions & 26 deletions contracts/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ pragma solidity 0.8.11;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
import "./utils/Pausable.sol";


import "./interfaces/IDepositExecute.sol";
import "./interfaces/IERCHandler.sol";
import "./interfaces/IGenericHandler.sol";
Expand All @@ -15,9 +17,15 @@ import "./interfaces/IAccessControlSegregator.sol";
@title Facilitates deposits and creation of deposit proposals, and deposit executions.
@author ChainSafe Systems.
*/
contract Bridge is Pausable, Context {
contract Bridge is Pausable, Context, EIP712 {
using ECDSA for bytes32;

bytes32 private constant _PROPOSALS_TYPEHASH =
keccak256("Proposals(Proposal[] proposals)Proposal(uint8 originDomainID,uint64 depositNonce,bytes32 resourceID,bytes data)");
bytes32 private constant _PROPOSAL_TYPEHASH =
keccak256("Proposal(uint8 originDomainID,uint64 depositNonce,bytes32 resourceID,bytes data)");


uint8 public immutable _domainID;
address public _MPCAddress;

Expand Down Expand Up @@ -96,7 +104,7 @@ contract Bridge is Pausable, Context {
@param domainID ID of chain the Bridge contract exists on.
@param accessControl Address of access control contract.
*/
constructor (uint8 domainID, address accessControl) public {
constructor (uint8 domainID, address accessControl) EIP712("Bridge", "3.1.0") public {
_domainID = domainID;
_accessControl = IAccessControlSegregator(accessControl);

Expand Down Expand Up @@ -271,37 +279,28 @@ contract Bridge is Pausable, Context {

/**
@notice Executes a deposit proposal using a specified handler contract (only if signature is signed by MPC).
@param originDomainID ID of chain deposit originated from.
@param resourceID ResourceID to be used when making deposits.
@param depositNonce ID of deposit generated by origin Bridge contract.
@param data Data originally provided when deposit was made.
@notice Failed executeProposal from handler don't revert, emits {FailedHandlerExecution} event.
@param proposal Proposal which consists of:
- originDomainID ID of chain deposit originated from.
- resourceID ResourceID to be used when making deposits.
- depositNonce ID of deposit generated by origin Bridge contract.
- data Data originally provided when deposit was made.
@param signature bytes memory signature composed of MPC key shares
@notice Emits {ProposalExecution} event.
@notice Behaviour of this function is different for {GenericHandler} and other specific ERC handlers.
In the case of ERC handler, when execution fails, the handler will terminate the function with revert.
In the case of {GenericHandler}, when execution fails, the handler will emit a failure event and terminate the function normally.
*/
function executeProposal(uint8 originDomainID, uint64 depositNonce, bytes calldata data, bytes32 resourceID, bytes calldata signature) public whenNotPaused {
require(isProposalExecuted(originDomainID, depositNonce) != true, "Deposit with provided nonce already executed");

address signer = keccak256(abi.encode(originDomainID, _domainID, depositNonce, data, resourceID)).recover(signature);
require(signer == _MPCAddress, "Invalid message signer");

address handler = _resourceIDToHandlerAddress[resourceID];
bytes32 dataHash = keccak256(abi.encodePacked(handler, data));
function executeProposal(Proposal memory proposal, bytes calldata signature) public {
Proposal[] memory proposalArray = new Proposal[](1);
proposalArray[0] = proposal;

IDepositExecute depositHandler = IDepositExecute(handler);

usedNonces[originDomainID][depositNonce / 256] |= 1 << (depositNonce % 256);

// Reverts for every handler except GenericHandler
depositHandler.executeProposal(resourceID, data);

emit ProposalExecution(originDomainID, depositNonce, dataHash);
executeProposals(proposalArray, signature);
}

/**
@notice Executes a batch of deposit proposals using a specified handler contract for each proposal (only if signature is signed by MPC).
@notice If executeProposals fails it doesn't revert, emits {FailedHandlerExecution} event.
@param proposals Array of Proposal which consists of:
- originDomainID ID of chain deposit originated from.
- resourceID ResourceID to be used when making deposits.
Expand All @@ -313,11 +312,9 @@ contract Bridge is Pausable, Context {
In the case of ERC handler, when execution fails, the handler will terminate the function with revert.
In the case of {GenericHandler}, when execution fails, the handler will emit a failure event and terminate the function normally.
*/
function executeProposals(Proposal[] memory proposals, bytes memory signature) public whenNotPaused {
function executeProposals(Proposal[] memory proposals, bytes calldata signature) public whenNotPaused {
require(proposals.length > 0, "Proposals can't be an empty array");

address signer = keccak256(abi.encode(proposals, _domainID)).recover(signature);
require(signer == _MPCAddress, "Invalid message signer");
require(verify(proposals, signature), "Invalid proposal signer");

for (uint256 i = 0; i < proposals.length; i++) {
if(isProposalExecuted(proposals[i].originDomainID, proposals[i].depositNonce)) {
Expand Down Expand Up @@ -399,4 +396,31 @@ contract Bridge is Pausable, Context {
function isProposalExecuted(uint8 domainID, uint256 depositNonce) public view returns (bool) {
return usedNonces[domainID][depositNonce / 256] & (1 << (depositNonce % 256)) != 0;
}

/**
@notice Verifies that proposal data is signed by MPC address.
@param proposals array of Proposals.
@param signature signature bytes memory signature composed of MPC key shares.
@return Boolean value depending if signer is vaild or not.
*/
function verify(Proposal[] memory proposals, bytes calldata signature) public view returns (bool) {
bytes32[] memory keccakData = new bytes32[](proposals.length);
for (uint256 i = 0; i < proposals.length; i++) {
keccakData[i] = keccak256(
abi.encode(
_PROPOSAL_TYPEHASH,
proposals[i].originDomainID,
proposals[i].depositNonce,
proposals[i].resourceID,
keccak256(proposals[i].data)
)
);
}

address signer = _hashTypedDataV4(
keccak256(abi.encode(
_PROPOSALS_TYPEHASH, keccak256(abi.encodePacked(keccakData))))
).recover(signature);
return signer == _MPCAddress;
}
}
70 changes: 33 additions & 37 deletions test/contractBridge/executeProposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ contract('Bridge - [execute proposal]', async (accounts) => {

let data = '';
let dataHash = '';
let proposal;

beforeEach(async () => {
await Promise.all([
Expand Down Expand Up @@ -71,6 +72,13 @@ contract('Bridge - [execute proposal]', async (accounts) => {
depositProposalData = Helpers.createERCDepositData(depositAmount, 20, recipientAddress)
depositProposalDataHash = Ethers.utils.keccak256(ERC20HandlerInstance.address + depositProposalData.substr(2));

proposal = {
originDomainID: originDomainID,
depositNonce: expectedDepositNonce,
resourceID: resourceID,
data: depositProposalData
};

// set MPC address to unpause the Bridge
await BridgeInstance.endKeygen(Helpers.mpcAddress);
});
Expand All @@ -79,10 +87,10 @@ contract('Bridge - [execute proposal]', async (accounts) => {
const destinationDomainID = await BridgeInstance._domainID();

assert.isFalse(await BridgeInstance.isProposalExecuted(destinationDomainID, expectedDepositNonce));
});
});

it('should create and execute executeProposal successfully', async () => {
const proposalSignedData = await Helpers.signDataWithMpc(originDomainID, destinationDomainID, expectedDepositNonce, depositProposalData, resourceID);
it('should create and execute executeProposal successfully', async () => {
const proposalSignedData = await Helpers.signTypedProposal(BridgeInstance.address, [proposal]);

// depositorAddress makes initial deposit of depositAmount
assert.isFalse(await BridgeInstance.paused());
Expand All @@ -95,12 +103,9 @@ contract('Bridge - [execute proposal]', async (accounts) => {
));

await TruffleAssert.passes(BridgeInstance.executeProposal(
originDomainID,
expectedDepositNonce,
depositProposalData,
resourceID,
proposalSignedData,
{ from: relayer1Address }
proposal,
proposalSignedData,
{ from: relayer1Address }
));

// check that deposit nonce has been marked as used in bitmap
Expand All @@ -111,8 +116,8 @@ contract('Bridge - [execute proposal]', async (accounts) => {
assert.strictEqual(recipientBalance.toNumber(), depositAmount);
});

it('should fail to executeProposal if deposit nonce is already used', async () => {
const proposalSignedData = await Helpers.signDataWithMpc(originDomainID, destinationDomainID, expectedDepositNonce, depositProposalData, resourceID);
it('should skip executing proposal if deposit nonce is already used', async () => {
const proposalSignedData = await Helpers.signTypedProposal(BridgeInstance.address, [proposal]);

// depositorAddress makes initial deposit of depositAmount
assert.isFalse(await BridgeInstance.paused());
Expand All @@ -125,26 +130,23 @@ contract('Bridge - [execute proposal]', async (accounts) => {
));

await TruffleAssert.passes(BridgeInstance.executeProposal(
originDomainID,
expectedDepositNonce,
depositProposalData,
resourceID,
proposal,
proposalSignedData,
{ from: relayer1Address }
));

await TruffleAssert.reverts(BridgeInstance.executeProposal(
originDomainID,
expectedDepositNonce,
depositProposalData,
resourceID,
proposalSignedData,
{ from: relayer1Address }
), "Deposit with provided nonce already executed");
const skipExecuteTx = await BridgeInstance.executeProposal(
proposal,
proposalSignedData,
{ from: relayer1Address }
);

// check that no ProposalExecution events are emitted
assert.equal(skipExecuteTx.logs.length, 0);
});

it('executeProposal event should be emitted with expected values', async () => {
const proposalSignedData = await Helpers.signDataWithMpc(originDomainID, destinationDomainID, expectedDepositNonce, depositProposalData, resourceID);
const proposalSignedData = await Helpers.signTypedProposal(BridgeInstance.address, [proposal]);

// depositorAddress makes initial deposit of depositAmount
assert.isFalse(await BridgeInstance.paused());
Expand All @@ -157,13 +159,10 @@ contract('Bridge - [execute proposal]', async (accounts) => {
));

const proposalTx = await BridgeInstance.executeProposal(
originDomainID,
expectedDepositNonce,
depositProposalData,
resourceID,
proposal,
proposalSignedData,
{ from: relayer1Address }
);
{ from: relayer1Address }
);

TruffleAssert.eventEmitted(proposalTx, 'ProposalExecution', (event) => {
return event.originDomainID.toNumber() === originDomainID &&
Expand All @@ -179,8 +178,8 @@ contract('Bridge - [execute proposal]', async (accounts) => {
assert.strictEqual(recipientBalance.toNumber(), depositAmount);
});

it('should fail to executeProposal if signed destinationDomainID in not the domain on which proposal should be executed', async () => {
const proposalSignedData = await Helpers.signDataWithMpc(originDomainID, invalidDestinationDomainID, expectedDepositNonce, depositProposalData, resourceID);
it('should fail to executeProposal if signed Proposal has different chainID than the one on which it should be executed', async () => {
const proposalSignedData = await Helpers.mockSignTypedProposalWithInvalidChainID(BridgeInstance.address, [proposal]);

// depositorAddress makes initial deposit of depositAmount
assert.isFalse(await BridgeInstance.paused());
Expand All @@ -193,12 +192,9 @@ contract('Bridge - [execute proposal]', async (accounts) => {
));

await TruffleAssert.reverts(BridgeInstance.executeProposal(
originDomainID,
expectedDepositNonce,
depositProposalData,
resourceID,
proposal,
proposalSignedData,
{ from: relayer1Address }
), "Invalid message signer");
), "Invalid proposal signer");
});
});
14 changes: 7 additions & 7 deletions test/contractBridge/executeProposals.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
});

it('should create and execute executeProposal successfully', async () => {
const proposalSignedData = await Helpers.signArrayOfDataWithMpc(proposalsForExecution, destinationDomainID);
const proposalSignedData = await Helpers.signTypedProposal(BridgeInstance.address, proposalsForExecution);

// depositorAddress makes initial deposit of depositAmount
assert.isFalse(await BridgeInstance.paused());
Expand Down Expand Up @@ -180,7 +180,7 @@
});

it('should skip executing proposal if deposit nonce is already used', async () => {
const proposalSignedData = await Helpers.signArrayOfDataWithMpc(proposalsForExecution, destinationDomainID);
const proposalSignedData = await Helpers.signTypedProposal(BridgeInstance.address, proposalsForExecution);

// depositorAddress makes initial deposit of depositAmount
assert.isFalse(await BridgeInstance.paused());
Expand Down Expand Up @@ -245,7 +245,7 @@
});

it('should fail executing proposals if empty array is passed for execution', async () => {
const proposalSignedData = await Helpers.signArrayOfDataWithMpc(proposalsForExecution, destinationDomainID);
const proposalSignedData = await Helpers.signTypedProposal(BridgeInstance.address, proposalsForExecution);

await TruffleAssert.reverts(BridgeInstance.executeProposals(
[],
Expand All @@ -255,7 +255,7 @@
});

it('executeProposal event should be emitted with expected values', async () => {
const proposalSignedData = await Helpers.signArrayOfDataWithMpc(proposalsForExecution, destinationDomainID);
const proposalSignedData = await Helpers.signTypedProposal(BridgeInstance.address, proposalsForExecution);

// depositorAddress makes initial deposit of depositAmount
assert.isFalse(await BridgeInstance.paused());
Expand Down Expand Up @@ -322,8 +322,8 @@
assert.strictEqual(recipientERC1155Balance.toNumber(), depositAmount);
});

it('should fail to executeProposals if signed destinationDomainID in not the domain on which proposal should be executed', async () => {
const proposalSignedData = await Helpers.signArrayOfDataWithMpc(proposalsForExecution, invalidDestinationDomainID);
it('should fail to executeProposals if signed Proposal has different chainID than the one on which it should be executed', async () => {
const proposalSignedData = await Helpers.mockSignTypedProposalWithInvalidChainID(BridgeInstance.address, proposalsForExecution);

// depositorAddress makes initial deposit of depositAmount
assert.isFalse(await BridgeInstance.paused());
Expand Down Expand Up @@ -355,6 +355,6 @@
proposalsForExecution,
proposalSignedData,
{ from: relayer1Address }
), "Invalid message signer");
), "Invalid proposal signer");
});
});
Loading

0 comments on commit fe8628b

Please sign in to comment.