Skip to content
This repository has been archived by the owner on Mar 2, 2023. It is now read-only.

feat: first backing commit #194

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
104 changes: 104 additions & 0 deletions contracts/backing/Backer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.12;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { BidPrice } from "./libraries/BidPrice.sol";

contract Backer is Ownable {
using BidPrice for uint256;

IERC20 public immutable numeraire;
IERC20 public immutable native;

mapping(address => bool) public purchaser;

constructor(address _numeraire, address _native) {
numeraire = IERC20(_numeraire);
native = IERC20(_native);
}

modifier nonZero(uint256 amount) {
if (amount == 0) revert Backer__Zero_Amount();
_;
}

modifier onlyPurchaser() {
if (!purchaser[msg.sender]) revert Backer__Not_Purchaser();
_;
}

/// ADMIN

/// @notice allows or de-allows purchaser to purchase tokens at the bid price
function togglePurchaser(address _purchaser) external onlyOwner {
purchaser[_purchaser] = !purchaser[_purchaser];
emit SetPurchaser(_purchaser, purchaser[_purchaser]);
}

/// PURCHASE

/// @notice purchases exact amount of native tokens at the bid price and sends them to recipient
/// @dev throws if msg.sender is not a purchaser
/// bid price in, so we round up
function purchaseExactNat(uint256 toPurchase, address recipient) external nonZero(toPurchase) onlyPurchaser {
uint256 bidPrice = toPurchase.computeBidPriceCeil(
numeraire.balanceOf(address(this)),
native.balanceOf(address(this)),
native.totalSupply()
);
numeraire.transferFrom(msg.sender, address(this), bidPrice);
native.transfer(recipient, toPurchase);
emit Purchased(bidPrice, toPurchase);
}

/// @notice purchases native tokens with an exact amount of numeraire tokens
/// to purchase out, so we round down
function purchaseExactNum(uint256 toSpend, address recipient) external onlyPurchaser {
uint256 toPurchase = toSpend.computeInverseBidPriceFloor(
numeraire.balanceOf(address(this)),
native.balanceOf(address(this)),
native.totalSupply()
);
numeraire.transferFrom(msg.sender, address(this), toSpend);
native.transfer(recipient, toPurchase);
emit Purchased(toSpend, toPurchase);
}

/// REDEEM

/// @notice redeems exact amount of native tokens for numeraire tokens and sends them to recipient
/// bid price out, so we round down
function redeemExactNat(uint256 toRedeem, address recipient) external {
uint256 bidPrice = toRedeem.computeBidPriceFloor(
numeraire.balanceOf(address(this)),
native.balanceOf(address(this)),
native.totalSupply()
);
native.transferFrom(msg.sender, address(this), toRedeem);
numeraire.transfer(recipient, bidPrice);
emit Redeemed(bidPrice, toRedeem);
}

/// @notice redeems exact amount of numeraire tokens for native tokens and sends them to recipient
/// toRedeem in, so we round up
function redeemExactNum(uint256 toObtain, address recipient) external nonZero(toObtain) {
uint256 toRedeem = toObtain.computeInverseBidPriceCeil(
numeraire.balanceOf(address(this)),
native.balanceOf(address(this)),
native.totalSupply()
);
native.transferFrom(msg.sender, address(this), toRedeem);
numeraire.transfer(recipient, toObtain);
emit Redeemed(toObtain, toRedeem);
}

error Backer__Purchase_Too_Much(uint256 balance);
error Backer__Impossible_Deposit();
error Backer__Zero_Amount();
error Backer__Not_Purchaser();

event SetPurchaser(address purchaser, bool canPurchase);
event Purchased(uint256 numeraireIn, uint256 nativeOut);
event Redeemed(uint256 numeraireOut, uint256 nativeIn);
}
70 changes: 70 additions & 0 deletions contracts/backing/libraries/BidPrice.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

library BidPrice {
/// @notice computes bid price, in numeraire tokens, of an amount of native tokens with floor division
/// @notice returns a number with numeraire token decimals
/// @dev throws if nativeSupply <= nativeBalance (impossible to redeem: no tokens outside);
/// @param amount the amount of native tokens to compute the price of (decimals: native)
/// @param numBalance the current numeraire token balance of the backing (decimals: numeraire)
/// @param nativeBalance the current native token balance of the backing (decimals: native)
/// @param nativeSupply the total native token supply(decimals: native)
function computeBidPriceFloor(
uint256 amount,
uint256 numBalance,
uint256 nativeBalance,
uint256 nativeSupply
) internal pure returns (uint256) {
if (amount > 0) return (amount * numBalance) / (nativeSupply - nativeBalance);
else return 0;
}

/// @notice computes bid price, in numeraire tokens, of an amount of native tokens with ceiling division
/// @notice returns a number with native token decimals
/// @dev throws if nativeSupply <= nativeBalance (impossible to redeem: no tokens outside)
/// @dev throws if numBalance = 0 (bid price 0 would mean free tokens);
/// @dev throws if amount = 0;
function computeBidPriceCeil(
uint256 amount,
uint256 numBalance,
uint256 nativeBalance,
uint256 nativeSupply
) internal pure returns (uint256) {
if (amount > 0) return 1 + (amount * numBalance - 1) / (nativeSupply - nativeBalance);
else return 0;
}

/// @notice computes how many native tokens is worth amount of numeraire tokens with floor division
/// @dev returns a number with native token decimals
/// @param amount the amount of numeraire tokens to compute the value of (decimals: numeraire)
/// @param numBalance the current numeraire token balance of the backing (decimals: numeraire)
/// @param nativeBalance the current native token balance of the backing (decimals: native)
/// @param nativeSupply the total native token supply(decimals: native)
/// @dev throws if numBalance = 0;
function computeInverseBidPriceFloor(
uint256 amount,
uint256 numBalance,
uint256 nativeBalance,
uint256 nativeSupply
) internal pure returns (uint256) {
if (amount > 0) return (amount * (nativeSupply - nativeBalance)) / numBalance;
else return 0;
}

/// @notice computes how many native tokens is worth amount of numeraire tokens with ceiling division
/// @dev returns a number with native token decimals
/// @param amount the amount of numeraire tokens to compute the value of (decimals: numeraire)
/// @param numBalance the current numeraire token balance of the backing (decimals: numeraire)
/// @param nativeBalance the current native token balance of the backing (decimals: native)
/// @param nativeSupply the total native token supply(decimals: native)
/// @dev throws if numBalance = 0, amount = 0 or nativeSupply <= nativeBalance;
function computeInverseBidPriceCeil(
uint256 amount,
uint256 numBalance,
uint256 nativeBalance,
uint256 nativeSupply
) internal pure returns (uint256) {
if (amount > 0) return 1 + (amount * (nativeSupply - nativeBalance) - 1) / numBalance;
else return 0;
}
}
37 changes: 37 additions & 0 deletions contracts/backing/libraries/FixedPoint.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

library FixedPoint {
// Fixed point number fixed to be 1e18;
uint256 constant ONE = 1e18;

/// @notice Fixed point multiplication when both a and b are supposed to have 18 decimals
/// @dev approximates down to the nearest fixed point number less than a * b
/// @dev throws if a * b >= 2^256
function mulDown(uint256 a, uint256 b) internal pure returns (uint256) {
return (a * b) / ONE;
}

/// @dev approximates down to the nearest fixed point number greater than a * b
/// @dev throws if a = 0 or b = 0
/// @dev throws if a * b >= 2^256
function mulUp(uint256 a, uint256 b) internal pure returns (uint256) {
return 1 + (a * b - 1) / ONE;
}

/// @notice Fixed point division when both a and b are supposed to have 18 decimals
/// @dev approximates down to the nearest fixed point number less than a / b
/// @dev throws if b = 0;
/// @dev throws if a > 2^256 / 10^18
function divDown(uint256 a, uint256 b) internal pure returns (uint256) {
return (a * ONE) / b;
}

/// @notice Fixed point division when both a and b are supposed to have 18 decimals
/// @dev approximates down to the nearest fixed point number less than a / b
/// @dev throws if a = 0 or b = 0;
/// @dev throws if a > 2^256 / 10^18
function divUp(uint256 a, uint256 b) internal pure returns (uint256) {
return 1 + (a * ONE - 1) / b;
}
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"dotenv": "^15.0.0",
"eslint": "^8.8.0",
"eslint-config-prettier": "^8.3.0",
"ethereumjs-util": "^7.1.5",
"ethereum-waffle": "^3.4.0",
"ethereumjs-util": "^7.1.5",
"ethers": "^5.5.4",
"fs-extra": "^10.0.0",
"hardhat": "^2.8.3",
Expand Down Expand Up @@ -94,5 +94,8 @@
"test": "yarn clean && yarn compile && hardhat test",
"typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain",
"verify": "hardhat verify"
},
"dependencies": {
"@nomicfoundation/hardhat-network-helpers": "^1.0.6"
}
}
105 changes: 105 additions & 0 deletions test/backing/Backing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { ethers, waffle } from "hardhat";
import { BigNumber } from "ethers";
import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address";
import { expect } from "chai";

import { Backer } from "../../src/types/Backer";
import { MockToken } from "../../src/types/MockToken";

describe("Backer", function () {
// We define a fixture to reuse the same setup in every test.
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployBacker() {
const [owner, purchaser, redeemer] = await ethers.getSigners();

const Token = await ethers.getContractFactory("MockToken");
const nativeToken = <MockToken>await Token.deploy("Native token", "NAT", 18);
const stablecoin = <MockToken>await Token.deploy("Stablecoin", "DAI", 18);

const Backer = await ethers.getContractFactory("Backer");
const backer = <Backer>await Backer.deploy(stablecoin.address, nativeToken.address);

return { backer, nativeToken, stablecoin, owner, purchaser, redeemer };
}

describe("Deployment", function () {
let backerContract: Backer;
let native: MockToken, numeraire: MockToken;
let admin: SignerWithAddress;
before("Load fixture", async () => {
const { backer, nativeToken, stablecoin, owner } = await loadFixture(deployBacker);
[backerContract, native, numeraire, admin] = [backer, nativeToken, stablecoin, owner];
});

it("Should set the right owner", async function () {
expect(await backerContract.owner()).to.equal(admin.address);
});

it("Should set the right native token", async function () {
expect(await backerContract.native()).to.equal(native.address);
});

it("Should set the right numeraire token", async function () {
expect(await backerContract.numeraire()).to.equal(numeraire.address);
});

// it("Other deployments checks", async function () {
// });
});

describe("Functions", function () {
let backerContract: Backer;
let native: MockToken, numeraire: MockToken;
let admin: SignerWithAddress, buyer: SignerWithAddress, seller: SignerWithAddress;
before("Load fixture", async () => {
const { backer, nativeToken, stablecoin, owner, purchaser, redeemer } = await loadFixture(deployBacker);
[backerContract, native, numeraire, admin, buyer, seller] = [
backer,
nativeToken,
stablecoin,
owner,
purchaser,
redeemer,
];
});
describe("Validations", function () {
it("Non-admin cannot set purchaser", async function () {
await expect(backerContract.connect(buyer).togglePurchaser(buyer.address)).to.be.reverted;
});

it("Cannot purchase more than own purchasable datum", async function () {
// mint some tokens to buyer
await numeraire.mintTo(buyer.address, BigNumber.from(1000));

await expect(backerContract.connect(buyer).purchaseExactNat(1, buyer.address)).to.be.reverted;
});

it("Cannot purchase zero amount", async function () {
// mint some tokens to buyer
await expect(backerContract.connect(buyer).purchaseExactNat(0, buyer.address)).to.be.reverted;
});

it("Cannot purchase more than backer balance", async function () {
// mint some tokens to buyer
const purchaseAmount = ethers.utils.parseUnits("1000", 18);
await numeraire.mintTo(buyer.address, purchaseAmount);

await backerContract.connect(admin).togglePurchaser(buyer.address);

await expect(backerContract.connect(buyer).purchaseExactNat(1, buyer.address)).to.be.reverted;
});

xit("Cannot redeem if does not have enough native tokens", async function () {});
});

describe("Events", function () {
xit("Event 1", async function () {});
});

describe("Actual functions", function () {
xit("Function 1", async function () {});
});
});
});
2 changes: 2 additions & 0 deletions test/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import type { TestStrategy } from "../src/types/TestStrategy";
import type { YearnStrategy } from "../src/types/YearnStrategy";
import type { MarginTradingStrategy } from "../src/types/MarginTradingStrategy";
import type { Vault } from "../src/types/Vault";
import type { Backer } from "../src/types/Backer";

declare module "mocha" {
export interface Context {
backer: Backer;
liquidator: Liquidator;
mockKyberNetworkProxy: MockKyberNetworkProxy;
mockToken: MockToken;
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,13 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"

"@nomicfoundation/hardhat-network-helpers@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.6.tgz#5e2026ddde5ca539f70a2bf498528afd08bd0827"
integrity sha512-a35iVD4ycF6AoTfllAnKm96IPIzzHpgKX/ep4oKc2bsUKFfMlacWdyntgC/7d5blyCTXfFssgNAvXDZfzNWVGQ==
dependencies:
ethereumjs-util "^7.1.4"

"@nomiclabs/hardhat-ethers@^2.0.4", "@nomiclabs/hardhat-ethers@^2.0.6":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.1.1.tgz#3f1d1ab49813d1bae4c035cc1adec224711e528b"
Expand Down