Skip to content

Commit

Permalink
Update EIP-5247 add assets (ethereum#6109)
Browse files Browse the repository at this point in the history
* Update EIP

* Add refimpl
  • Loading branch information
xinbenlv authored Dec 9, 2022
1 parent 2802db1 commit 6fa8101
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 0 deletions.
69 changes: 69 additions & 0 deletions EIPS/eip-5247.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,75 @@ interface IERC5247 {
* Arrays were used for `target`s, `value`s, `calldata`s instead of single variables, allowing a proposal to carry arbitrarily long multiple functional calls.
* `registeredProposalId` is returned in `createProposal` so the standard can support implementation to decide their own format of proposal id.

## Test Cases

A simple test case can be found as

```ts
it("Should work for a simple case", async function () {
const { contract, erc721, owner } = await loadFixture(deployFixture);
const callData1 = erc721.interface.encodeFunctionData("mint", [owner.address, 1]);
const callData2 = erc721.interface.encodeFunctionData("mint", [owner.address, 2]);
await contract.connect(owner)
.createProposal(
0,
[erc721.address, erc721.address],
[0,0],
[0,0],
[callData1, callData2],
[]);
expect(await erc721.balanceOf(owner.address)).to.equal(0);
await contract.connect(owner).executeProposal(0, []);
expect(await erc721.balanceOf(owner.address)).to.equal(2);
});
```

See [testProposalRegistry.ts](../assets/eip-5247/testProposalRegistry.ts) for the whole testset.

## Reference Implementation

A simple reference implementation can be found.

```solidity
function createProposal(
uint256 proposalId,
address[] calldata targets,
uint256[] calldata values,
uint256[] calldata gasLimits,
bytes[] calldata calldatas,
bytes calldata extraParams
) external returns (uint256 registeredProposalId) {
require(targets.length == values.length, "GeneralForwarder: targets and values length mismatch");
require(targets.length == gasLimits.length, "GeneralForwarder: targets and gasLimits length mismatch");
require(targets.length == calldatas.length, "GeneralForwarder: targets and calldatas length mismatch");
registeredProposalId = proposalCount;
proposalCount++;
proposals[registeredProposalId] = Proposal({
by: msg.sender,
proposalId: proposalId,
targets: targets,
values: values,
calldatas: calldatas,
gasLimits: gasLimits
});
emit ProposalCreated(msg.sender, proposalId, targets, values, gasLimits, calldatas, extraParams);
return registeredProposalId;
}
function executeProposal(uint256 proposalId, bytes calldata extraParams) external {
Proposal storage proposal = proposals[proposalId];
address[] memory targets = proposal.targets;
string memory errorMessage = "Governor: call reverted without message";
for (uint256 i = 0; i < targets.length; ++i) {
(bool success, bytes memory returndata) = proposal.targets[i].call{value: proposal.values[i]}(proposal.calldatas[i]);
Address.verifyCallResult(success, returndata, errorMessage);
}
emit ProposalExecuted(msg.sender, proposalId, extraParams);
}
```

See [ProposalRegistry.sol](../assets/eip-5247/ProposalRegistry.sol) for more information.

## Security Considerations

Needs discussion.
Expand Down
65 changes: 65 additions & 0 deletions assets/eip-5247/ProposalRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT
// A fully runnalbe version can be found in https://github.com/ercref/ercref-contracts/tree/869843f23dc4da793f0d9d018ed92e3950da8f75
pragma solidity ^0.8.17;

import "./IERC5247.sol";
import "@openzeppelin/contracts/utils/Address.sol";

struct Proposal {
address by;
uint256 proposalId;
address[] targets;
uint256[] values;
uint256[] gasLimits;
bytes[] calldatas;
}

contract ProposalRegistry is IERC5247 {
using Address for address;
mapping(uint256 => Proposal) public proposals;
uint256 private proposalCount;
function createProposal(
uint256 proposalId,
address[] calldata targets,
uint256[] calldata values,
uint256[] calldata gasLimits,
bytes[] calldata calldatas,
bytes calldata extraParams
) external returns (uint256 registeredProposalId) {
require(targets.length == values.length, "GeneralForwarder: targets and values length mismatch");
require(targets.length == gasLimits.length, "GeneralForwarder: targets and gasLimits length mismatch");
require(targets.length == calldatas.length, "GeneralForwarder: targets and calldatas length mismatch");
registeredProposalId = proposalCount;
proposalCount++;

proposals[registeredProposalId] = Proposal({
by: msg.sender,
proposalId: proposalId,
targets: targets,
values: values,
calldatas: calldatas,
gasLimits: gasLimits
});
emit ProposalCreated(msg.sender, proposalId, targets, values, gasLimits, calldatas, extraParams);
return registeredProposalId;
}
function executeProposal(uint256 proposalId, bytes calldata extraParams) external {
Proposal storage proposal = proposals[proposalId];
address[] memory targets = proposal.targets;
string memory errorMessage = "Governor: call reverted without message";
for (uint256 i = 0; i < targets.length; ++i) {
(bool success, bytes memory returndata) = proposal.targets[i].call{value: proposal.values[i]}(proposal.calldatas[i]);
Address.verifyCallResult(success, returndata, errorMessage);
}
emit ProposalExecuted(msg.sender, proposalId, extraParams);
}

function getProposal(uint256 proposalId) external view returns (Proposal memory) {
return proposals[proposalId];
}

function getProposalCount() external view returns (uint256) {
return proposalCount;
}

}
162 changes: 162 additions & 0 deletions assets/eip-5247/testProposalRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// A fully runnalbe version can be found in https://github.com/ercref/ercref-contracts/tree/869843f23dc4da793f0d9d018ed92e3950da8f75
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { hexlify } from "ethers/lib/utils";
import { ethers } from "hardhat";

describe("ProposalRegistry", function () {
async function deployFixture() {
// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners();

const ProposalRegistry = await ethers.getContractFactory("ProposalRegistry");
const contract = await ProposalRegistry.deploy();

const ERC721ForTesting = await ethers.getContractFactory("ERC721ForTesting");
const erc721 = await ERC721ForTesting.deploy();

const SimpleForwarder = await ethers.getContractFactory("SimpleForwarder");
const forwarder = await SimpleForwarder.deploy();
return { contract, erc721, forwarder, owner, otherAccount };
}

describe("Deployment", function () {
it("Should work for a simple case", async function () {
const { contract, erc721, owner } = await loadFixture(deployFixture);
const callData1 = erc721.interface.encodeFunctionData("mint", [owner.address, 1]);
const callData2 = erc721.interface.encodeFunctionData("mint", [owner.address, 2]);
await contract.connect(owner)
.createProposal(
0,
[erc721.address, erc721.address],
[0,0],
[0,0],
[callData1, callData2],
[]);
expect(await erc721.balanceOf(owner.address)).to.equal(0);
await contract.connect(owner).executeProposal(0, []);
expect(await erc721.balanceOf(owner.address)).to.equal(2);
});
const Ns = [0, 50, 100, 150, 200];
for (let n of Ns) {

it(`Should work for a proposal case of ${n}`, async function () {
const { contract, erc721, owner } = await loadFixture(deployFixture);
const numOfMint = n;
const calldatas = [];
for (let i = 0 ; i < numOfMint; i++) {
const callData = erc721.interface.encodeFunctionData("mint", [owner.address, i]);
calldatas.push(callData);
}
let txCreate = await contract.connect(owner)
.createProposal(
0,
Array(numOfMint).fill(erc721.address),
Array(numOfMint).fill(0),
Array(numOfMint).fill(0),
calldatas,
[]);
let txCreateWaited = await txCreate.wait();
console.log(`Creation TX gas`, txCreateWaited.cumulativeGasUsed.toString());
console.log(`Gas per mint`, parseInt(txCreateWaited.cumulativeGasUsed.toString()) / numOfMint);
expect(await erc721.balanceOf(owner.address)).to.equal(0);
let txExecute = await contract.connect(owner).executeProposal(0, []);
let txExecuteWaited = await txExecute.wait();
console.log(`Execution TX gas`, txExecuteWaited.cumulativeGasUsed.toString());
console.log(`Gas per mint`, parseInt(txExecuteWaited.cumulativeGasUsed.toString()) / numOfMint);
expect(await erc721.balanceOf(owner.address)).to.equal(numOfMint);
});
}
});
describe("Benchmark", function () {
it(`Should work for a forwarding case`, async function () {
const { forwarder, erc721, owner } = await loadFixture(deployFixture);
const numOfMint = 200;
const calldatas = [];
for (let i = 0 ; i < numOfMint; i++) {
const callData = erc721.interface.encodeFunctionData("mint", [owner.address, i]);
calldatas.push(callData);
}
expect(await erc721.balanceOf(owner.address)).to.equal(0);
let txForward = await forwarder.connect(owner)
.forward(
Array(numOfMint).fill(erc721.address),
Array(numOfMint).fill(0),
Array(numOfMint).fill(0),
calldatas);
let txForwardWaited = await txForward.wait();

console.log(`txForwardWaited TX gas`, txForwardWaited.cumulativeGasUsed.toString());

console.log(`Gas per mint`, parseInt(txForwardWaited.cumulativeGasUsed.toString()) / numOfMint);
expect(await erc721.balanceOf(owner.address)).to.equal(numOfMint);

});


it(`Should work for erc721 batchMint with same addresses`, async function () {
const { erc721, owner } = await loadFixture(deployFixture);
const numOfMint = 200;
const tokenIds = [];
const addresses = [];

for (let i = 0 ; i < numOfMint; i++) {
addresses.push(owner.address);// addresses.push(hexlify(ethers.utils.randomBytes(20)));
tokenIds.push(i);
}
const tx = await erc721.connect(owner).batchMint(addresses, tokenIds);
const txWaited = await tx.wait();
console.log(`batchMint TX gas`, txWaited.cumulativeGasUsed.toString());
console.log(`At ${numOfMint} Gas per mint`, parseInt(txWaited.cumulativeGasUsed.toString()) / numOfMint);
})

it(`Should work for erc721 batchMint with different addresses`, async function () {
const { erc721, owner } = await loadFixture(deployFixture);
const numOfMint = 200;
const tokenIds = [];
const addresses = [];

for (let i = 0 ; i < numOfMint; i++) {
addresses.push(hexlify(ethers.utils.randomBytes(20)));
tokenIds.push(i);
}
const tx = await erc721.connect(owner).batchMint(addresses, tokenIds);
const txWaited = await tx.wait();
console.log(`batchMint TX gas`, txWaited.cumulativeGasUsed.toString());
console.log(`At ${numOfMint} Gas per mint`, parseInt(txWaited.cumulativeGasUsed.toString()) / numOfMint);
});


it(`Should work for erc721 batchSafeMint with same addresses`, async function () {
const { erc721, owner } = await loadFixture(deployFixture);
const numOfMint = 400;
const tokenIds = [];
const addresses = [];

for (let i = 0 ; i < numOfMint; i++) {
addresses.push(owner.address);// addresses.push(hexlify(ethers.utils.randomBytes(20)));
tokenIds.push(i);
}
const tx = await erc721.connect(owner).batchSafeMint(addresses, tokenIds);
const txWaited = await tx.wait();
console.log(`batchSafeMint TX gas`, txWaited.cumulativeGasUsed.toString());
console.log(`At ${numOfMint} Gas per mint`, parseInt(txWaited.cumulativeGasUsed.toString()) / numOfMint);
});

it(`Should work for erc721 batchSafeMint with different addresses`, async function () {
const { erc721, owner } = await loadFixture(deployFixture);
const numOfMint = 400;
const tokenIds = [];
const addresses = [];

for (let i = 0 ; i < numOfMint; i++) {
addresses.push(hexlify(ethers.utils.randomBytes(20)));
tokenIds.push(i);
}
const tx = await erc721.connect(owner).batchSafeMint(addresses, tokenIds);
const txWaited = await tx.wait();
console.log(`batchSafeMint TX gas`, txWaited.cumulativeGasUsed.toString());
console.log(`At ${numOfMint} the Gas per mint`, parseInt(txWaited.cumulativeGasUsed.toString()) / numOfMint);
});
});
});

0 comments on commit 6fa8101

Please sign in to comment.