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

Add FV specification for ERC20Wrapper #4100

Merged
merged 15 commits into from
Mar 8, 2023
5 changes: 5 additions & 0 deletions .changeset/tender-needles-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC20Wrapper`: self wrapping and deposit by the wrapper itself are now explicitelly forbiden.
3 changes: 1 addition & 2 deletions certora/harnesses/ERC20PermitHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

pragma solidity ^0.8.0;

import "../patched/token/ERC20/ERC20.sol";
import "../patched/token/ERC20/extensions/ERC20Permit.sol";

contract ERC20PermitHarness is ERC20, ERC20Permit {
contract ERC20PermitHarness is ERC20Permit {
constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {}

function mint(address account, uint256 amount) external {
Expand Down
25 changes: 25 additions & 0 deletions certora/harnesses/ERC20WrapperHarness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../patched/token/ERC20/extensions/ERC20Wrapper.sol";

contract ERC20WrapperHarness is ERC20Wrapper {
constructor(IERC20 _underlying, string memory _name, string memory _symbol) ERC20(_name, _symbol) ERC20Wrapper(_underlying) {}

function underlyingTotalSupply() public view returns (uint256) {
return underlying().totalSupply();
}

function underlyingBalanceOf(address account) public view returns (uint256) {
return underlying().balanceOf(account);
}

function underlyingAllowanceToThis(address account) public view returns (uint256) {
return underlying().allowance(account, address(this));
}

function recover(address account) public returns (uint256) {
return _recover(account);
}
}
8 changes: 4 additions & 4 deletions certora/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ if (request) {
}

for (const { spec, contract, files, options = [] } of Object.values(specs)) {
limit(runCertora, spec, contract, files, [...options, ...extraOptions]);
limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...extraOptions]);
}

// Run certora, aggregate the output and print it at the end
Expand All @@ -50,7 +50,7 @@ async function runCertora(spec, contract, files, options = []) {
const urls = data.toString('utf8').match(/https?:\S*/g);
for (const url of urls ?? []) {
if (url.includes('/jobStatus/')) {
console.error(`[${spec}] ${url}`);
console.error(`[${spec}] ${url.replace('/jobStatus/', '/output/')}`);
frangio marked this conversation as resolved.
Show resolved Hide resolved
stream.off('data', logStatusUrl);
break;
}
Expand Down Expand Up @@ -108,8 +108,8 @@ function writeEntry(spec, contract, success, url) {
spec,
contract,
success ? ':x:' : ':heavy_check_mark:',
`[link](${url})`,
`[link](${url?.replace('/jobStatus/', '/output/')})`,
url ? `[link](${url})` : 'error',
url ? `[link](${url?.replace('/jobStatus/', '/output/')})` : 'error',
),
);
}
12 changes: 12 additions & 0 deletions certora/specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@
],
"options": ["--optimistic_loop"]
},
{
"spec": "ERC20Wrapper",
"contract": "ERC20WrapperHarness",
"files": [
"certora/harnesses/ERC20PermitHarness.sol",
"certora/harnesses/ERC20WrapperHarness.sol"
],
"options": [
"--link ERC20WrapperHarness:_underlying=ERC20PermitHarness",
"--optimistic_loop"
]
},
{
"spec": "Initializable",
"contract": "InitializableHarness",
Expand Down
198 changes: 198 additions & 0 deletions certora/specs/ERC20Wrapper.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import "helpers.spec"
import "ERC20.spec"

methods {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we specify an invariant, is it only checked for the methods listed in this block? What happens if we add a new function in the Solidity code and we don't add it here?

I just noticed this in the docs and I find it concerning!

It is possible for a method signature to appear in the methods block but not in the contract being verified. In this case, the Prover will skip any rules that mention the missing method, rather than reporting an error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK:

  • The invariant (and arbitrary methods) will check all functions in the contract's ABI, even if not here.
  • Putting them here allow to mark them envfree, of set a summarize policy
  • If a rule does a test on a function declared here, its absence from the contract will not break things and the tests will just be ignored.

underlying() returns(address) envfree
underlyingTotalSupply() returns(uint256) envfree
underlyingBalanceOf(address) returns(uint256) envfree
underlyingAllowanceToThis(address) returns(uint256) envfree

depositFor(address, uint256) returns(bool)
withdrawTo(address, uint256) returns(bool)
recover(address) returns(uint256)
}

use invariant totalSupplyIsSumOfBalances

/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Helper: consequence of `totalSupplyIsSumOfBalances` applied to underlying │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
function underlyingBalancesLowerThanUnderlyingSupply(address a) returns bool {
return underlyingBalanceOf(a) <= underlyingTotalSupply();
}

function sumOfUnderlyingBalancesLowerThanUnderlyingSupply(address a, address b) returns bool {
return a != b => underlyingBalanceOf(a) + underlyingBalanceOf(b) <= underlyingTotalSupply();
}

/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Invariant: wrapped token can't be undercollateralized (solvency of the wrapper) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
invariant totalSupplyIsSmallerThanUnderlyingBalance()
totalSupply() <= underlyingBalanceOf(currentContract) &&
underlyingBalanceOf(currentContract) <= underlyingTotalSupply() &&
underlyingTotalSupply() <= max_uint256
{
preserved {
requireInvariant totalSupplyIsSumOfBalances;
require underlyingBalancesLowerThanUnderlyingSupply(currentContract);
}
preserved depositFor(address account, uint256 amount) with (env e) {
require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(e.msg.sender, currentContract);
}
}

invariant noSelfWrap()
currentContract != underlying()

/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Rule: depositFor liveness and effects │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule depositFor(env e) {
require nonpayable(e);

address sender = e.msg.sender;
address receiver;
address other;
uint256 amount;

// sanity
requireInvariant noSelfWrap;
requireInvariant totalSupplyIsSumOfBalances;
requireInvariant totalSupplyIsSmallerThanUnderlyingBalance;
require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(currentContract, sender);

uint256 balanceBefore = balanceOf(receiver);
uint256 supplyBefore = totalSupply();
uint256 senderUnderlyingBalanceBefore = underlyingBalanceOf(sender);
uint256 senderUnderlyingAllowanceBefore = underlyingAllowanceToThis(sender);
uint256 wrapperUnderlyingBalanceBefore = underlyingBalanceOf(currentContract);
uint256 underlyingSupplyBefore = underlyingTotalSupply();

uint256 otherBalanceBefore = balanceOf(other);
uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other);

depositFor@withrevert(e, receiver, amount);
bool success = !lastReverted;

// liveness
assert success <=> (
sender != currentContract && // invalid sender
sender != 0 && // invalid sender
receiver != 0 && // invalid receiver
amount <= senderUnderlyingBalanceBefore && // deposit doesn't exceed balance
amount <= senderUnderlyingAllowanceBefore // deposit doesn't exceed allowance
);

// effects
assert success => (
balanceOf(receiver) == balanceBefore + amount &&
totalSupply() == supplyBefore + amount &&
underlyingBalanceOf(currentContract) == wrapperUnderlyingBalanceBefore + amount &&
underlyingBalanceOf(sender) == senderUnderlyingBalanceBefore - amount
);

// no side effect
assert underlyingTotalSupply() == underlyingSupplyBefore;
assert balanceOf(other) != otherBalanceBefore => other == receiver;
assert underlyingBalanceOf(other) != otherUnderlyingBalanceBefore => (other == sender || other == currentContract);
}

/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Rule: withdrawTo liveness and effects │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule withdrawTo(env e) {
require nonpayable(e);

address sender = e.msg.sender;
address receiver;
address other;
uint256 amount;

// sanity
requireInvariant noSelfWrap;
requireInvariant totalSupplyIsSumOfBalances;
requireInvariant totalSupplyIsSmallerThanUnderlyingBalance;
require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(currentContract, receiver);

uint256 balanceBefore = balanceOf(sender);
uint256 supplyBefore = totalSupply();
uint256 receiverUnderlyingBalanceBefore = underlyingBalanceOf(receiver);
uint256 wrapperUnderlyingBalanceBefore = underlyingBalanceOf(currentContract);
uint256 underlyingSupplyBefore = underlyingTotalSupply();

uint256 otherBalanceBefore = balanceOf(other);
uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other);

withdrawTo@withrevert(e, receiver, amount);
bool success = !lastReverted;

// liveness
assert success <=> (
sender != 0 && // invalid sender
receiver != 0 && // invalid receiver
amount <= balanceBefore // withdraw doesn't exceed balance
);

// effects
assert success => (
balanceOf(sender) == balanceBefore - amount &&
totalSupply() == supplyBefore - amount &&
underlyingBalanceOf(currentContract) == wrapperUnderlyingBalanceBefore - (currentContract != receiver ? amount : 0) &&
underlyingBalanceOf(receiver) == receiverUnderlyingBalanceBefore + (currentContract != receiver ? amount : 0)
);

// no side effect
assert underlyingTotalSupply() == underlyingSupplyBefore;
assert balanceOf(other) != otherBalanceBefore => other == sender;
assert underlyingBalanceOf(other) != otherUnderlyingBalanceBefore => (other == receiver || other == currentContract);
}

/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Rule: recover liveness and effects │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule recover(env e) {
require nonpayable(e);

address receiver;
address other;

// sanity
requireInvariant noSelfWrap;
requireInvariant totalSupplyIsSumOfBalances;
requireInvariant totalSupplyIsSmallerThanUnderlyingBalance;

uint256 value = underlyingBalanceOf(currentContract) - totalSupply();
uint256 supplyBefore = totalSupply();
uint256 balanceBefore = balanceOf(receiver);

uint256 otherBalanceBefore = balanceOf(other);
uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other);

recover@withrevert(e, receiver);
bool success = !lastReverted;

// liveness
assert success <=> receiver != 0;

// effect
assert success => (
balanceOf(receiver) == balanceBefore + value &&
totalSupply() == supplyBefore + value &&
totalSupply() == underlyingBalanceOf(currentContract)
);

// no side effect
assert underlyingBalanceOf(other) == otherUnderlyingBalanceBefore;
assert balanceOf(other) != otherBalanceBefore => other == receiver;
}
5 changes: 4 additions & 1 deletion contracts/token/ERC20/extensions/ERC20Wrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ abstract contract ERC20Wrapper is ERC20 {
IERC20 private immutable _underlying;

constructor(IERC20 underlyingToken) {
require(underlyingToken != this, "ERC20Wrapper: cannot self wrap");
_underlying = underlyingToken;
}

Expand All @@ -44,7 +45,9 @@ abstract contract ERC20Wrapper is ERC20 {
* @dev Allow a user to deposit underlying tokens and mint the corresponding number of wrapped tokens.
*/
function depositFor(address account, uint256 amount) public virtual returns (bool) {
SafeERC20.safeTransferFrom(_underlying, _msgSender(), address(this), amount);
address sender = _msgSender();
require(sender != address(this), "ERC20Wrapper: wrapper can't deposit");
SafeERC20.safeTransferFrom(_underlying, sender, address(this), amount);
_mint(account, amount);
return true;
}
Expand Down