Skip to content
This repository has been archived by the owner on Oct 26, 2022. It is now read-only.

Eip712 fix #84

Merged
merged 5 commits into from
Mar 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
"interfaces/",
"./oracles/ChainlinkOracle.sol",
"./oracles/CompoundOracle.sol",
"./samples/salary.sol",
"BentoHelper.sol",
],
providerOptions: {
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ which led to the creation of an internal audit checklist (see checks.txt in the

Contracts are covered 100% by tests.

Formal verification is done using Certora.
Formal verification is done using Certora. All reported issues were fixed.

## Licence

Expand Down
6 changes: 4 additions & 2 deletions contracts/MasterContractManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ contract MasterContractManager is BoringOwnable, BoringFactory {
assembly {
chainId := chainid()
}
DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, "BentoBox V2", chainId, address(this)));
DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, keccak256("BentoBox V1"), chainId, address(this)));
}

/// @notice Other contracts need to register with this master contract so that users can approve them for the BentoBox.
Expand Down Expand Up @@ -97,7 +97,9 @@ contract MasterContractManager is BoringOwnable, BoringFactory {
keccak256(
abi.encode(
APPROVAL_SIGNATURE_HASH,
approved ? "Give FULL access to funds in (and approved to) BentoBox?" : "Revoke access to BentoBox?",
approved
? keccak256("Give FULL access to funds in (and approved to) BentoBox?")
: keccak256("Revoke access to BentoBox?"),
user,
masterContract,
approved,
Expand Down
6 changes: 4 additions & 2 deletions contracts/flat/BentoBoxFlat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ contract MasterContractManager is BoringOwnable, BoringFactory {
assembly {
chainId := chainid()
}
DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, "BentoBox V2", chainId, address(this)));
DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, keccak256("BentoBox V1"), chainId, address(this)));
}

function registerProtocol() public {
Expand Down Expand Up @@ -496,7 +496,9 @@ contract MasterContractManager is BoringOwnable, BoringFactory {
keccak256(
abi.encode(
APPROVAL_SIGNATURE_HASH,
approved ? "Give FULL access to funds in (and approved to) BentoBox?" : "Revoke access to BentoBox?",
approved
? keccak256("Give FULL access to funds in (and approved to) BentoBox?")
: keccak256("Revoke access to BentoBox?"),
user,
masterContract,
approved,
Expand Down
204 changes: 204 additions & 0 deletions contracts/samples/salary.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
pragma experimental ABIEncoderV2;
import "../BentoBox.sol";

// solhint-disable not-rely-on-time

// IDEA: Make changes to salaries, funder or recipient
// IDEA: Enable partial withdrawals

contract Salary is BoringBatchable {
using BoringMath for uint256;

BentoBox public bentoBox;

event LogCreate(
address indexed funder,
address indexed recipient,
IERC20 indexed token,
uint32 cliffTimestamp,
uint32 endTimestamp,
uint32 cliffPercent,
uint128 totalShares,
uint256 salaryId
);
event LogWithdraw(uint256 indexed salaryId, address indexed to, uint256 shares);
event LogCancel(uint256 indexed salaryId, address indexed to, uint256 shares);

constructor(BentoBox _bentoBox) public {
bentoBox = _bentoBox;
_bentoBox.registerProtocol();
}

// Included to be able to approve BentoBox and create in the same transaction (using batch)
function setBentoBoxApproval(
address user,
bool approved,
uint8 v,
bytes32 r,
bytes32 s
) public {
bentoBox.setMasterContractApproval(user, address(this), approved, v, r, s);
}

/// now cliffTimestamp
/// | | endTimestamp
/// V V |
/// ------------------------------- |
/// | ^ ^ | V
/// | | cliffPercent |
/// | | V |
/// | | -----> |
/// | | \
/// | totalShares \
/// | | \
/// | | \
/// | V \
/// -----------------------------------------
struct UserSalary {
// The recipient of the salary
address recipient;
// The ERC20 token
IERC20 token;
// The amount of shares that the recipient has already withdrawn
uint256 withdrawnShares;
// The timestamp of the cliff (also the start of the slope)
uint32 cliffTimestamp;
// The timestamp of the end of vesting (the end of the slope)
uint32 endTimestamp;
// The cliff payout in percent of the shares, 1e18 = 100%
uint64 cliffPercent;
// The total payout in shares
uint128 shares;
}

/// Array of all salaries managed by the contract
UserSalary[] public salaries;
/// The funder of each salary, separated out for gas optimization
address[] public funder;

uint8 private constant MODE_BENTO = 0; // Use BentoBox balance
uint8 private constant MODE_ERC20_SKIM = 1; // Use ERC20 tokens deposited onto the BentoBox contract
uint8 private constant MODE_ERC20 = 2; // Use ERC20 tokens in the users wallet (transferFrom with approval)

/// Create a salary
function create(
address recipient,
IERC20 token,
uint32 cliffTimestamp,
uint32 endTimestamp,
uint32 cliffPercent,
uint8 mode,
uint128 amount
) public returns (uint256 salaryId, uint256 shares) {
// Check that the end if after or equal to the cliff
// If they are equal, all shares become payable at once, use this for a fixed term lockup
require(cliffTimestamp <= endTimestamp, "Salary: cliff > end");
// You cannot have a cliff greater than 100%, important check, without the contract will lose funds
require(cliffPercent <= 1e18, "Salary: cliff too large");

if (mode == MODE_BENTO) {
// Fund this salary using the funder's BentoBox balance. Convert the amoutn to shares, then transfer the shares
shares = bentoBox.toShare(token, amount, false);
bentoBox.transfer(token, msg.sender, address(this), shares);
} else {
// Fund this salary with ERC20 tokens
// This is a potential reentrancy target, funds in this contract could be higher than the total of salaries during this call
// Since this contract doesn't have a skim function, this is ok
(, shares) = bentoBox.deposit(token, mode == MODE_ERC20_SKIM ? address(bentoBox) : msg.sender, address(this), amount, 0);
}

salaryId = salaries.length;
UserSalary memory salary;
salary.recipient = recipient;
salary.token = token;
salary.cliffTimestamp = cliffTimestamp;
salary.endTimestamp = endTimestamp;
salary.cliffPercent = cliffPercent;
salary.shares = shares.to128();
salaries.push(salary);
funder.push(msg.sender);

emit LogCreate(msg.sender, recipient, token, cliffTimestamp, endTimestamp, cliffPercent, shares.to128(), salaryId);
}

function _available(UserSalary memory salary) internal view returns (uint256 shares) {
if (block.timestamp < salary.cliffTimestamp) {
// Before the cliff, none is available
shares = 0;
} else if (block.timestamp >= salary.endTimestamp) {
// After the end, all is available
shares = salary.shares;
} else {
// In between, cliff is available, rest according to slope

// Time that has passed since the cliff
uint256 timeSinceCliff = block.timestamp.sub(salary.cliffTimestamp);
// Total time period of the slope
uint256 timeSlope = uint256(salary.endTimestamp).sub(salary.cliffTimestamp);
uint256 payablePercent = salary.cliffPercent;
if (timeSinceCliff > 0) {
// The percentage paid out during the slope
uint256 slopePercent = uint256(1e18).sub(uint256(salary.cliffPercent));
// The percentage payable on the slope added to the cliff percentage
payablePercent = payablePercent.add(slopePercent.mul(timeSinceCliff) / timeSlope);
}
// The share payable
shares = uint256(salary.shares).mul(payablePercent) / 1e18;
}

// Remove any shares already wiythdrawn, if negative, return 0
if (shares > salary.withdrawnShares) {
shares = shares.sub(salary.withdrawnShares);
} else {
shares = 0;
}
}

// Get the number of shares currently available for withdrawal by salaryId
function available(uint256 salaryId) public view returns (uint256 shares) {
shares = _available(salaries[salaryId]);
}

// Withdraw the maximum amount possible for a salaryId
function withdraw(
uint256 salaryId,
address to,
bool toBentoBox
) public {
UserSalary memory salary = salaries[salaryId];
// Only pay out to the recipient
require(salary.recipient == msg.sender, "Salary: not recipient");

uint256 pendingShares = _available(salary);
salaries[salaryId].withdrawnShares = salary.withdrawnShares.add(pendingShares);
if (toBentoBox) {
bentoBox.transfer(salary.token, address(this), to, pendingShares);
} else {
bentoBox.withdraw(salary.token, address(this), to, 0, pendingShares);
}
emit LogWithdraw(salaryId, to, pendingShares);
}

// Modifier for functions only allowed by the funder
modifier onlyFunder(uint256 salaryId) {
require(funder[salaryId] == msg.sender, "Salary: not funder");
_;
}

// Cancel a salary, can only be done by the funder
function cancel(
uint256 salaryId,
address to,
bool toBentoBox
) public onlyFunder(salaryId) {
uint256 sharesLeft = uint256(salaries[salaryId].shares).sub(salaries[salaryId].withdrawnShares);
if (toBentoBox) {
bentoBox.transfer(salaries[salaryId].token, address(this), to, sharesLeft);
} else {
bentoBox.withdraw(salaries[salaryId].token, address(this), to, 0, sharesLeft);
}
emit LogCancel(salaryId, to, sharesLeft);
}
}
4 changes: 2 additions & 2 deletions spec/harness/RebaseWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ contract RebaseWrapper {

Rebase public rebase;

function getElastic() public view returns (uint256) {
function getElastic() public view returns (uint128) {
return rebase.elastic;
}

function getBase() public view returns (uint256) {
function getBase() public view returns (uint128) {
return rebase.base;
}

Expand Down
4 changes: 2 additions & 2 deletions spec/rebase.spec
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
*/

methods {
getElastic() returns (uint256) envfree;
getBase() returns (uint256) envfree;
getElastic() returns (uint128) envfree;
getBase() returns (uint128) envfree;

toBase(uint256 elastic, bool roundUp) returns (uint256 base) envfree;
toBaseFloor(uint256 elastic) returns (uint256 base) envfree;
Expand Down
15 changes: 11 additions & 4 deletions test/utilities/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,26 @@ function getApprovalMsg(tokenAddress, approve, nonce, deadline) {
function getBentoBoxDomainSeparator(address, chainId) {
return keccak256(
defaultAbiCoder.encode(
["bytes32", "string", "uint256", "address"],
[keccak256(toUtf8Bytes("EIP712Domain(string name,uint256 chainId,address verifyingContract)")), "BentoBox V2", chainId, address]
["bytes32", "bytes32", "uint256", "address"],
[
keccak256(toUtf8Bytes("EIP712Domain(string name,uint256 chainId,address verifyingContract)")),
keccak256(toUtf8Bytes("BentoBox V1")),
chainId,
address,
]
)
)
}

function getBentoBoxApprovalDigest(bentoBox, user, masterContractAddress, approved, nonce, chainId = 1) {
const DOMAIN_SEPARATOR = getBentoBoxDomainSeparator(bentoBox.address, chainId)
const msg = defaultAbiCoder.encode(
["bytes32", "string", "address", "address", "bool", "uint256"],
["bytes32", "bytes32", "address", "address", "bool", "uint256"],
[
BENTOBOX_MASTER_APPROVAL_TYPEHASH,
approved ? "Give FULL access to funds in (and approved to) BentoBox?" : "Revoke access to BentoBox?",
approved
? keccak256(toUtf8Bytes("Give FULL access to funds in (and approved to) BentoBox?"))
: keccak256(toUtf8Bytes("Revoke access to BentoBox?")),
user.address,
masterContractAddress,
approved,
Expand Down