Smart Contract Executable Proposal Interface
Abstract
This EIP presents an interface for “smart contract executable proposals”: proposals that are submitted to, recorded on, and possibly executed on-chain. Such proposals include a series of information about function calls including the target contract address, ether value to be transmitted, gas limits and calldatas.
Motivation
It is oftentimes necessary to separate the code that is to be executed from the actual execution of the code.
A typical use case for this EIP is in a Decentralized Autonomous Organization (DAO). A proposer will create a smart proposal and advocate for it. Members will then choose whether or not to endorse the proposal and vote accordingly (see ERC-1202
). Finallym when consensus has been formed, the proposal is executed.
A second typical use-case is that one could have someone who they trust, such as a delegator, trustee, or an attorney-in-fact, or any bilateral collaboration format, where a smart proposal will be first composed, discussed, approved in some way, and then put into execution.
A third use-case is that a person could make an “offer” to a second person, potentially with conditions. The smart proposal can be presented as an offer and the second person can execute it if they choose to accept this proposal.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
interface IERC5247 {
event ProposalCreated(
address indexed proposer,
uint256 indexed proposalId,
address[] targets,
uint256[] values,
uint256[] gasLimits,
bytes[] calldatas,
bytes extraParams
);
event ProposalExecuted(
address indexed executor,
uint256 indexed proposalId,
bytes extraParams
);
function createProposal(
uint256 proposalId,
address[] calldata targets,
uint256[] calldata values,
uint256[] calldata gasLimits,
bytes[] calldata calldatas,
bytes calldata extraParams
) external returns (uint256 registeredProposalId);
function executeProposal(uint256 proposalId, bytes calldata extraParams) external;
}
Rationale
- Originally, this interface was part of part of
ERC-1202
. However, the proposal itself can potentially have many use cases outside of voting. It is possible that voting may not need to be upon a proposal in any particular format. Hence, we decide to decouple the voting interface and proposal interface. - 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 increateProposal
so the standard can support implementation to decide their own format of proposal id.
Test Cases
A simple test case can be found as
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 for the whole testset.
Reference Implementation
A simple reference implementation can be found.
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 for more information.
Security Considerations
Needs discussion.
Copyright
Copyright and related rights waived via CC0.