Solidity – Understanding the Checks-Effects-Interactions Design Pattern

contract-designdesign-patternsSecuritysolidity

I was workig on design pattern Checks-Effects-Interactions Pattern and found the below link which has a similar question but in the direction which I am looking at ..

Checks-Effects-Interactions Pattern – External Call not reached

In Checks-Effects-Interactions Pattern, what if there will be an error while making or during external call..

How the state of the contract will be managed? Do we need to make some checks and balances of system will manage itself.

Regards

Best Answer

Ultimately, the state of your contract is the contract's responsibility, so it falls on the author to design in the assurances.

It might be helpful to break down the concerns to address with each step.

Checks

Is the input acceptable? If not, then either "Fail Early and fail hard" or return false. As a default approach, fail hard ensures the entire transaction reverts including all functions in all contracts that were involved. This relieves the caller of the responsibility for dealing with the "failed" case.

require(someCondition);

Effects

Optimistically update the contract to a new valid state assuming that interactions will be successful. This protects the contract from re-entrance and race conditions. For emphasis, it must be a completely valid new state.

balance[msg.sender] = 0;

Interactions

.send(), .transfer() and .call() as well as function calls to "untrusted" contracts. You should generally limit interactions to one "untrusted" contract. You must check the result and the compiler issues a warning when you don't.

If the called contract "fails hard" with require() or revert() then there is nothing to check. The whole thing will revert if something goes wrong. This is one of the advantages of using transfer()to send ETH. If the contract returns(bool success) then you need to check for the false condition and decide what to do. This will always involve undoing the "optimistic accounting". If you want to carry on, then manually unwind it.

if(!msg.sender.send(amount)) {
   balances[msg.sender] += amount; // because we zeroed it out in step 2

If you do not want to carry on, you can revert the whole thing and return to the original state.

require(msg.sender.send(amount)); // this is what `transfer` does

Use it also for calling contract functions. If they return something, check it.

require(otherContract.doSomething()); // where it returns a bool

or

bool success = otherContract.doSomething();
if(!success) { 
  // time to undo the "optimistic" accounting (effects) if we want to continue 

If a transaction runs out of gas, the transaction will revert.

Much more could be said about the rationale for the pattern and non-obvious vulnerabilities that can emerge in contracts.

Hope it helps.

Related Topic