Skip to content

Instantly share code, notes, and snippets.

@hbarcelos
Last active March 22, 2022 20:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hbarcelos/5e95237114fed2471dbe26d9a318dec8 to your computer and use it in GitHub Desktop.
Save hbarcelos/5e95237114fed2471dbe26d9a318dec8 to your computer and use it in GitHub Desktop.
RwaLiquidationOracle2 and RwaUrn2
// Copyright (C) 2020, 2021 Lev Livnev <lev@liv.nev.org.uk>
// Copyright (C) 2022 Dai Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.6.12;
import {VatAbstract} from "dss-interfaces/dss/VatAbstract.sol";
import {DSValue} from "ds-value/value.sol";
/**
* @author Lev Livnev <lev@liv.nev.org.uk>
* @author Henrique Barcelos <henrique@clio.finance>
* @title An Oracle for liquitation of real-world assets (RWA).
* @dev This contract differs from the original [RwaLiquidationOracle](https://github.com/makerdao/MIP21-RWA-Example/blob/fce06885ff89d10bf630710d4f6089c5bba94b4d/src/RwaLiquidationOracle.sol)
* because `bump()` is allowed to decrease the value of the underlying asset.
* @dev One instance of contract can be used for many RWA collateral types.
*/
contract RwaLiquidationOracle2 {
/**
* @notice Ilk metadata struct
* @dev 4-member struct:
* @member string hash, of borrower's agrrement with MakerDAO.
* @member address pip, An Oracle for liquitation of real-world assets (RWA).
* @member uint48 tau, remediation period.
* @member uint48 toc, timestamp when liquidation was initiated.
*/
struct Ilk {
string doc;
address pip;
uint48 tau;
uint48 toc;
}
/// @notice Core module address.
VatAbstract public immutable vat;
/// @notice Module that handles system debt and surplus.
address public vow;
/// @notice All collateral types supported by this oracle. `ilks[ilk]`
mapping(bytes32 => Ilk) public ilks;
/// @notice Addresses with admin access on this contract. `wards[usr]`
mapping(address => uint256) public wards;
/**
* @notice `usr` was granted admin access.
* @param usr The user address.
*/
event Rely(address indexed usr);
/**
* @notice `usr` admin access was revoked.
* @param usr The user address.
*/
event Deny(address indexed usr);
/**
* @notice A contract parameter was updated.
* @param what The changed parameter name. Currently the only supported value is "vow".
* @param data The new value of the parameter.
*/
event File(bytes32 indexed what, address data);
/**
* @notice A new collateral `ilk` was added.
* @param ilk The name of the collateral.
* @param val The initial value for the price feed.
* @param doc The hash to the off-chain agreement for the ilk.
* @param tau The amount of time the ilk can remain in liquidation before being written-off.
*/
event Init(bytes32 indexed ilk, uint256 val, string doc, uint48 tau);
/**
* @notice The value of the collateral `ilk` was updated.
* @param ilk The name of the collateral.
* @param val The new value.
*/
event Bump(bytes32 indexed ilk, uint256 val);
/**
* @notice The liquidation process for collateral `ilk` was started.
* @param ilk The name of the collateral.
*/
event Tell(bytes32 indexed ilk);
/**
* @notice The liquidation process for collateral `ilk` was stopped before the write-off.
* @param ilk The name of the collateral.
*/
event Cure(bytes32 indexed ilk);
/**
* @notice A `urn` outstanding debt for collateral `ilk` was written-off.
* @param ilk The name of the collateral.
* @param urn The address of the urn.
*/
event Cull(bytes32 indexed ilk, address indexed urn);
/**
* @param vat_ The core module address.
* @param vow_ The address of module that handles system debt and surplus.
*/
constructor(address vat_, address vow_) public {
vat = VatAbstract(vat_);
vow = vow_;
wards[msg.sender] = 1;
emit Rely(msg.sender);
emit File("vow", vow_);
}
/*//////////////////////////////////
Authorization
//////////////////////////////////*/
/**
* @notice Grants `usr` admin access to this contract.
* @param usr The user address.
*/
function rely(address usr) external auth {
wards[usr] = 1;
emit Rely(usr);
}
/**
* @notice Revokes `usr` admin access from this contract.
* @param usr The user address.
*/
function deny(address usr) external auth {
wards[usr] = 0;
emit Deny(usr);
}
modifier auth() {
require(wards[msg.sender] == 1, "RwaOracle/not-authorized");
_;
}
/*//////////////////////////////////
Administration
//////////////////////////////////*/
/**
* @notice Updates a contract parameter.
* @param what The changed parameter name. Currently the only supported value is "vow".
* @param data The new value of the parameter.
*/
function file(bytes32 what, address data) external auth {
if (what == "vow") {
vow = data;
} else {
revert("RwaOracle/unrecognised-param");
}
emit File(what, data);
}
/**
* @notice Initializes a new collateral type `ilk`.
* @param ilk The name of the collateral type.
* @param val The initial value for the price feed.
* @param doc The hash to the off-chain agreement for the ilk.
* @param tau The amount of time the ilk can remain in liquidation before being written-off.
*/
function init(
bytes32 ilk,
uint256 val,
string calldata doc,
uint48 tau
) external auth {
// doc, and tau can be amended, but tau cannot decrease
require(tau >= ilks[ilk].tau, "RwaOracle/decreasing-tau");
ilks[ilk].doc = doc;
ilks[ilk].tau = tau;
if (ilks[ilk].pip == address(0)) {
DSValue pip = new DSValue();
ilks[ilk].pip = address(pip);
pip.poke(bytes32(val));
} else {
val = uint256(DSValue(ilks[ilk].pip).read());
}
emit Init(ilk, val, doc, tau);
}
/*//////////////////////////////////
Operations
//////////////////////////////////*/
/**
* @notice Performs valuation adjustment for a given ilk.
* @param ilk The ilk to adjust.
* @param val The new value.
*/
function bump(bytes32 ilk, uint256 val) external auth {
DSValue pip = DSValue(ilks[ilk].pip);
require(address(pip) != address(0), "RwaOracle/unknown-ilk");
require(ilks[ilk].toc == 0, "RwaOracle/in-remediation");
pip.poke(bytes32(val));
emit Bump(ilk, val);
}
/**
* @notice Enables liquidation for a given ilk.
* @param ilk The ilk being liquidated.
*/
function tell(bytes32 ilk) external auth {
require(ilks[ilk].pip != address(0), "RwaOracle/unknown-ilk");
(, , , uint256 line, ) = vat.ilks(ilk);
require(line == 0, "RwaOracle/nonzero-line");
ilks[ilk].toc = uint48(block.timestamp);
emit Tell(ilk);
}
/**
* @notice Remediation: stops the liquidation process for a given ilk.
* @param ilk The ilk being remediated.
*/
function cure(bytes32 ilk) external auth {
require(ilks[ilk].pip != address(0), "RwaOracle/unknown-ilk");
require(ilks[ilk].toc > 0, "RwaOracle/not-in-liquidation");
ilks[ilk].toc = 0;
emit Cure(ilk);
}
/**
* @notice Writes-off a specific urn for a given ilk.
* @dev It assigns the outstanding debt of the urn to the vow.
* @param ilk The ilk being liquidated.
* @param urn The urn being written-off.
*/
function cull(bytes32 ilk, address urn) external auth {
require(ilks[ilk].pip != address(0), "RwaOracle/unknown-ilk");
require(block.timestamp >= DSMathCustom.add(ilks[ilk].toc, ilks[ilk].tau), "RwaOracle/early-cull");
DSValue(ilks[ilk].pip).poke(bytes32(0));
(uint256 ink, uint256 art) = vat.urns(ilk, urn);
vat.grab(ilk, urn, address(this), vow, -int256(ink), -int256(art));
emit Cull(ilk, urn);
}
/**
* @notice Allows off-chain parties to check the state of the loan.
* @param ilk the Ilk.
*/
function good(bytes32 ilk) external view returns (bool) {
require(ilks[ilk].pip != address(0), "RwaOracle/unknown-ilk");
return (ilks[ilk].toc == 0 || block.timestamp < DSMathCustom.add(ilks[ilk].toc, ilks[ilk].tau));
}
}
/**
* @title An extension/subset of `DSMath` containing only the methods required in this file.
*/
library DSMathCustom {
function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x + y) >= x, "DSMath/add-overflow");
}
function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
require(y == 0 || (z = x * y) / y == x, "DSMath/mul-overflow");
}
}
// Copyright (C) 2020, 2021 Lev Livnev <lev@liv.nev.org.uk>
// Copyright (C) 2022 Dai Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.6.8 <0.7.0;
import {DSTest} from "ds-test/test.sol";
import {DSToken} from "ds-token/token.sol";
import {DSMath} from "ds-math/math.sol";
import {DSValue} from "ds-value/value.sol";
import {Vat} from "dss/vat.sol";
import {Jug} from "dss/jug.sol";
import {Spotter} from "dss/spot.sol";
import {DaiJoin} from "dss/join.sol";
import {AuthGemJoin} from "dss-gem-joins/join-auth.sol";
import {OFHTokenLike} from "./tokens/ITokenWrapper.sol";
import {TokenWrapper} from "./tokens/TokenWrapper.sol";
import {MockOFH} from "./tokens/mocks/MockOFH.sol";
import {RwaInputConduit2} from "./RwaInputConduit2.sol";
import {RwaOutputConduit2} from "./RwaOutputConduit2.sol";
import {RwaUrn2} from "./RwaUrn2.sol";
import {RwaLiquidationOracle2} from "./RwaLiquidationOracle2.sol";
interface Hevm {
function warp(uint256) external;
function store(
address,
bytes32,
bytes32
) external;
}
contract TokenUser {
DSToken internal immutable dai;
constructor(DSToken dai_) public {
dai = dai_;
}
function transfer(address who, uint256 wad) external {
dai.transfer(who, wad);
}
}
contract TryCaller {
function doCall(address addr, bytes memory data) external returns (bool) {
assembly {
let ok := call(gas(), addr, 0, add(data, 0x20), mload(data), 0, 0)
let free := mload(0x40)
mstore(free, ok)
mstore(0x40, add(free, 32))
revert(free, 32)
}
}
function tryCall(address addr, bytes calldata data) external returns (bool ok) {
(, bytes memory returned) = address(this).call(abi.encodeWithSignature("doCall(address,bytes)", addr, data));
ok = abi.decode(returned, (bool));
}
}
contract RwaOperator is TryCaller {
RwaUrn2 internal urn;
RwaOutputConduit2 internal outC;
RwaInputConduit2 internal inC;
constructor(
RwaUrn2 urn_,
RwaOutputConduit2 outC_,
RwaInputConduit2 inC_
) public {
urn = urn_;
outC = outC_;
inC = inC_;
}
function approve(
TokenWrapper tok,
address who,
uint256 wad
) public {
tok.approve(who, wad);
}
function pick(address who) public {
outC.pick(who);
}
function lock(uint256 wad) public {
urn.lock(wad);
}
function free(uint256 wad) public {
urn.free(wad);
}
function draw(uint256 wad) public {
urn.draw(wad);
}
function wipe(uint256 wad) public {
urn.wipe(wad);
}
function canPick(address who) public returns (bool) {
return this.tryCall(address(outC), abi.encodeWithSignature("pick(address)", who));
}
function canDraw(uint256 wad) public returns (bool) {
return this.tryCall(address(urn), abi.encodeWithSignature("draw(uint256)", wad));
}
function canFree(uint256 wad) public returns (bool) {
return this.tryCall(address(urn), abi.encodeWithSignature("free(uint256)", wad));
}
}
contract RwaMate is TryCaller {
RwaOutputConduit2 internal outC;
RwaInputConduit2 internal inC;
constructor(RwaOutputConduit2 outC_, RwaInputConduit2 inC_) public {
outC = outC_;
inC = inC_;
}
function pushOut() public {
return outC.push();
}
function pushIn() public {
return inC.push();
}
function canPushOut() public returns (bool) {
return this.tryCall(address(outC), abi.encodeWithSignature("push()"));
}
function canPushIn() public returns (bool) {
return this.tryCall(address(inC), abi.encodeWithSignature("push()"));
}
}
contract RwaLiquidationOracle2Test is DSTest, DSMath {
bytes20 internal constant CHEAT_CODE = bytes20(uint160(uint256(keccak256("hevm cheat code"))));
Hevm internal hevm;
DSToken internal dai;
TokenWrapper internal wrapper;
MockOFH internal token;
Vat internal vat;
Jug internal jug;
Spotter internal spotter;
address internal constant VOW = address(123);
DaiJoin internal daiJoin;
AuthGemJoin internal gemJoin;
RwaLiquidationOracle2 internal oracle;
RwaUrn2 internal urn;
RwaOutputConduit2 internal outConduit;
RwaInputConduit2 internal inConduit;
RwaOperator internal op;
RwaMate internal mate;
TokenUser internal rec;
// Debt ceiling of 1000 DAI
string internal constant DOC = "Please sign this";
uint256 internal constant CEILING = 400 ether;
uint256 internal constant EIGHT_PCT = 1000000002440418608258400030;
uint48 internal constant TAU = 2 weeks;
function rad(uint256 wad) internal pure returns (uint256) {
return wad * RAY;
}
function setUp() public {
hevm = Hevm(address(CHEAT_CODE));
hevm.warp(104411200);
token = new MockOFH(400);
wrapper = new TokenWrapper(address(token));
wrapper.hope(address(this));
vat = new Vat();
jug = new Jug(address(vat));
jug.file("vow", VOW);
vat.rely(address(jug));
dai = new DSToken("Dai");
daiJoin = new DaiJoin(address(vat), address(dai));
vat.rely(address(daiJoin));
dai.setOwner(address(daiJoin));
vat.init("RWA007");
vat.file("Line", 100 * rad(CEILING));
vat.file("RWA007", "line", rad(CEILING));
jug.init("RWA007");
jug.file("RWA007", "duty", EIGHT_PCT);
oracle = new RwaLiquidationOracle2(address(vat), VOW);
oracle.init("RWA007", 1.1 ether, DOC, TAU);
vat.rely(address(oracle));
(, address pip, , ) = oracle.ilks("RWA007");
spotter = new Spotter(address(vat));
vat.rely(address(spotter));
spotter.file("RWA007", "mat", RAY);
spotter.file("RWA007", "pip", pip);
spotter.poke("RWA007");
gemJoin = new AuthGemJoin(address(vat), "RWA007", address(wrapper));
vat.rely(address(gemJoin));
outConduit = new RwaOutputConduit2(address(dai));
urn = new RwaUrn2(
address(vat),
address(jug),
address(gemJoin),
address(daiJoin),
address(outConduit),
400 ether
);
gemJoin.rely(address(urn));
inConduit = new RwaInputConduit2(address(dai), address(urn));
op = new RwaOperator(urn, outConduit, inConduit);
mate = new RwaMate(outConduit, inConduit);
rec = new TokenUser(dai);
// Wraps all tokens into `op` balance
token.transfer(address(wrapper), 400);
wrapper.wrap(address(op), 400);
urn.hope(address(op));
inConduit.mate(address(mate));
outConduit.mate(address(mate));
outConduit.hope(address(op));
op.approve(wrapper, address(urn), type(uint256).max);
}
function testCure() public {
op.lock(400 ether);
assertTrue(op.canDraw(1 ether));
// Flashes the liquidation beacon
vat.file("RWA007", "line", 0);
oracle.tell("RWA007");
assertTrue(!op.canDraw(10 ether));
// Advances time before the remediation period expires
hevm.warp(block.timestamp + TAU / 2);
oracle.cure("RWA007");
vat.file("RWA007", "line", rad(CEILING));
assertTrue(oracle.good("RWA007"));
assertEq(dai.balanceOf(address(rec)), 0);
op.draw(100 ether);
op.pick(address(rec));
mate.pushOut();
assertEq(dai.balanceOf(address(rec)), 100 ether);
}
function testFailCureUnknownIlk() public {
oracle.cure("ecma");
}
function testFailCureNotInRemediation() public {
oracle.cure("RWA007");
}
function testFailCureLiquidationCancelled() public {
op.lock(400 ether);
assertTrue(op.canDraw(1 ether));
// Flashes the liquidation beacon
vat.file("RWA007", "line", 0);
oracle.tell("RWA007");
// Borrowing not possible anymore
assertTrue(!op.canDraw(1 ether));
// Still in remediation period
hevm.warp(block.timestamp + TAU / 2);
assertTrue(oracle.good("RWA007"));
// Cancels liquidation
oracle.cure("RWA007");
vat.file("RWA007", "line", rad(CEILING));
assertTrue(oracle.good("RWA007"));
oracle.cure("RWA007");
}
function testCull() public {
// Lock the gem
op.lock(400 ether);
op.draw(200 ether);
// Flashes the liquidation beacon
vat.file("RWA007", "line", 0);
oracle.tell("RWA007");
hevm.warp(block.timestamp + TAU + 1 days);
assertEq(vat.gem("RWA007", address(oracle)), 0);
assertTrue(!oracle.good("RWA007"));
oracle.cull("RWA007", address(urn));
assertTrue(!op.canDraw(1 ether));
spotter.poke("RWA007");
(, , uint256 spot, , ) = vat.ilks("RWA007");
assertEq(spot, 0);
(uint256 ink, uint256 art) = vat.urns("RWA007", address(urn));
assertEq(ink, 0);
assertEq(art, 0);
// The system debt is equal to the drawn amount
assertEq(vat.sin(VOW), rad(200 ether));
// After the write-off, the gem goes to the oracle
assertEq(vat.gem("RWA007", address(oracle)), 400 ether);
}
function testUnremediedLoanIsNotGood() public {
op.lock(400 ether);
op.draw(100 ether);
vat.file("RWA007", "line", 0);
oracle.tell("RWA007");
assertTrue(oracle.good("RWA007"));
hevm.warp(block.timestamp + TAU + 1 days);
assertTrue(!oracle.good("RWA007"));
}
function testCullMultipleUrns() public {
RwaUrn2 urn2 = new RwaUrn2(
address(vat),
address(jug),
address(gemJoin),
address(daiJoin),
address(outConduit),
400 ether
);
gemJoin.rely(address(urn2));
RwaOperator op2 = new RwaOperator(urn2, outConduit, inConduit);
op.approve(wrapper, address(this), type(uint256).max);
wrapper.transferFrom(address(op), address(op2), 200 ether);
op2.approve(wrapper, address(urn2), type(uint256).max);
urn2.hope(address(op2));
op.lock(200 ether);
op.draw(50 ether);
op2.lock(200 ether);
op2.draw(80 ether);
assertTrue(op.canDraw(1 ether));
assertTrue(op2.canDraw(1 ether));
vat.file("RWA007", "line", 0);
oracle.tell("RWA007");
assertTrue(!op.canDraw(1 ether));
assertTrue(!op2.canDraw(1 ether));
hevm.warp(block.timestamp + TAU + 1 days);
oracle.cull("RWA007", address(urn));
assertEq(vat.sin(VOW), rad(50 ether));
oracle.cull("RWA007", address(urn2));
assertEq(vat.sin(VOW), rad(50 ether + 80 ether));
}
function testBumpCanIncreasePrice() public {
// Bump the price of RWA007
oracle.bump("RWA007", wmul(2 ether, 1.1 ether));
spotter.poke("RWA007");
(, address pip, , ) = oracle.ilks("RWA007");
(bytes32 value, bool exists) = DSValue(pip).peek();
assertEq(uint256(value), wmul(2 ether, 1.1 ether));
assertTrue(exists);
}
function testPriceIncreaseExtendsDrawingLimit() public {
op.lock(400 ether);
op.draw(CEILING);
op.pick(address(rec));
mate.pushOut();
// Debt ceiling was reached
assertTrue(!op.canDraw(1 ether));
// Increase the debt ceiling
vat.file("RWA007", "line", rad(CEILING + 200 ether));
// Still can't borrow much more because vault is unsafe
assertTrue(op.canDraw(1 ether));
assertTrue(!op.canDraw(200 ether));
// Bump the price of RWA007
oracle.bump("RWA007", wmul(2 ether, 1.1 ether));
spotter.poke("RWA007");
op.draw(200 ether);
op.pick(address(rec));
mate.pushOut();
assertEq(dai.balanceOf(address(rec)), CEILING + 200 ether);
}
function testBumpCanDecreasePrice() public {
// Bump the price of RWA007
oracle.bump("RWA007", wmul(0.5 ether, 1.1 ether));
spotter.poke("RWA007");
(, address pip, , ) = oracle.ilks("RWA007");
(bytes32 value, bool exists) = DSValue(pip).peek();
assertEq(uint256(value), wmul(0.5 ether, 1.1 ether));
assertTrue(exists);
}
function testPriceDecreaseReducesDrawingLimit() public {
op.lock(400 ether);
op.draw(200 ether);
op.pick(address(rec));
mate.pushOut();
// Still can borrow up to the ceiling
assertTrue(op.canDraw(200));
// Bump the price of RWA007
oracle.bump("RWA007", wmul(0.5 ether, 1.1 ether));
spotter.poke("RWA007");
// Cannot draw anymore because the decrease on the price
assertTrue(!op.canDraw(100 ether));
}
function testFailBumpUnknownIlk() public {
oracle.bump("ecma", wmul(2 ether, 1.1 ether));
}
function testFailBumpDuringLiquidation() public {
vat.file("RWA007", "line", 0);
oracle.tell("RWA007");
oracle.bump("RWA007", wmul(2 ether, 1.1 ether));
}
}
// Copyright (C) 2020, 2021 Lev Livnev <lev@liv.nev.org.uk>
// Copyright (C) 2022 Dai Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.6.8 <0.7.0;
import {VatAbstract, JugAbstract, DSTokenAbstract, GemJoinAbstract, DaiJoinAbstract, DaiAbstract} from "dss-interfaces/Interfaces.sol";
/**
* @author Lev Livnev <lev@liv.nev.org.uk>
* @author Kaue Cano <kaue@clio.finance>
* @title RwaUrn2: A capped vault for Real-World Assets (RWA).
* @dev This vault implements `gemCap`, the maximum amount of gem the urn can hold.
*/
contract RwaUrn2 {
/// @notice Addresses with admin access on this contract. `wards[usr]`
mapping(address => uint256) public wards;
/// @notice Addresses with operator access on this contract. `can[usr]`
mapping(address => uint256) public can;
/// @notice Core module address.
VatAbstract public vat;
/// @notice The stability fee management module.
JugAbstract public jug;
/// @notice The GemJoin adapter for the gem in this urn.
GemJoinAbstract public gemJoin;
/// @notice The adapter to mint/burn Dai tokens.
DaiJoinAbstract public daiJoin;
/// @notice The destination of Dai drawn from this urn.
address public outputConduit;
/// @notice Maximum amount of tokens this contract can lock
uint256 public gemCap;
/**
* @notice `usr` was granted admin access.
* @param usr The user address.
*/
event Rely(address indexed usr);
/**
* @notice `usr` admin access was revoked.
* @param usr The user address.
*/
event Deny(address indexed usr);
/**
* @notice `usr` was granted operator access.
* @param usr The user address.
*/
event Hope(address indexed usr);
/**
* @notice `usr` operator address was revoked.
* @param usr The user address.
*/
event Nope(address indexed usr);
/**
* @notice A contract parameter was updated.
* @param what The changed parameter name. Currently the supported values are: "outputConduit" and "jug".
* @param data The new value of the parameter.
*/
event File(bytes32 indexed what, address data);
/**
* @notice A contract parameter was updated.
* @param what The changed parameter name. Currently the supported values are: "gemCap".
* @param data The new value of the parameter.
*/
event File(bytes32 indexed what, uint256 data);
/**
* @notice `wad` amount of the gem was locked in the contract by `usr`.
* @param usr The operator address.
* @param wad The amount locked.
*/
event Lock(address indexed usr, uint256 wad);
/**
* @notice `wad` amount of the gem was freed the contract by `usr`.
* @param usr The operator address.
* @param wad The amount freed.
*/
event Free(address indexed usr, uint256 wad);
/**
* @notice `wad` amount of Dai was drawn by `usr` into `outputConduit`.
* @param usr The operator address.
* @param wad The amount drawn.
*/
event Draw(address indexed usr, uint256 wad);
/**
* @notice `wad` amount of Dai was repaid by `usr`.
* @param usr The operator address.
* @param wad The amount repaid.
*/
event Wipe(address indexed usr, uint256 wad);
/**
* @notice The urn outstanding balance was flushed out to `outputConduit`.
* @dev This can happen only after `cage()` has been called on the `Vat`.
* @param usr The operator address.
* @param wad The amount flushed out.
*/
event Quit(address indexed usr, uint256 wad);
modifier auth() {
require(wards[msg.sender] == 1, "RwaUrn2/not-authorized");
_;
}
modifier operator() {
require(can[msg.sender] == 1, "RwaUrn2/not-operator");
_;
}
/**
* @param vat_ Core module address.
* @param jug_ GemJoin adapter for the gem in this urn.
* @param gemJoin_ Adapter to mint/burn Dai tokens.
* @param daiJoin_ Stability fee management module.
* @param outputConduit_ Destination of Dai drawn from this urn.
* @param gemCap_ Maximum gem amount this urn can lock.
*/
constructor(
address vat_,
address jug_,
address gemJoin_,
address daiJoin_,
address outputConduit_,
uint256 gemCap_
) public {
require(outputConduit_ != address(0), "RwaUrn2/invalid-conduit");
require(gemCap_ > 0, "RwaUrn2/invalid-gemcap");
vat = VatAbstract(vat_);
jug = JugAbstract(jug_);
gemJoin = GemJoinAbstract(gemJoin_);
daiJoin = DaiJoinAbstract(daiJoin_);
outputConduit = outputConduit_;
gemCap = gemCap_;
wards[msg.sender] = 1;
DSTokenAbstract(GemJoinAbstract(gemJoin_).gem()).approve(gemJoin_, type(uint256).max);
DaiAbstract(DaiJoinAbstract(daiJoin_).dai()).approve(daiJoin_, type(uint256).max);
VatAbstract(vat_).hope(daiJoin_);
emit Rely(msg.sender);
emit File("outputConduit", outputConduit_);
emit File("jug", jug_);
emit File("gemCap", gemCap_);
}
/*//////////////////////////////////
Authorization
//////////////////////////////////*/
/**
* @notice Grants `usr` admin access to this contract.
* @param usr The user address.
*/
function rely(address usr) external auth {
wards[usr] = 1;
emit Rely(usr);
}
/**
* @notice Revokes `usr` admin access from this contract.
* @param usr The user address.
*/
function deny(address usr) external auth {
wards[usr] = 0;
emit Deny(usr);
}
/**
* @notice Grants `usr` operator access to this contract.
* @param usr The user address.
*/
function hope(address usr) external auth {
can[usr] = 1;
emit Hope(usr);
}
/**
* @notice Revokes `usr` operator access from this contract.
* @param usr The user address.
*/
function nope(address usr) external auth {
can[usr] = 0;
emit Nope(usr);
}
/*//////////////////////////////////
Administration
//////////////////////////////////*/
/**
* @notice Updates a contract parameter.
* @param what The changed parameter name. `"outputConduit" | "jug"`
* @param data The new value of the parameter.
*/
function file(bytes32 what, address data) external auth {
if (what == "outputConduit") {
require(data != address(0), "RwaUrn2/invalid-conduit");
outputConduit = data;
} else if (what == "jug") {
jug = JugAbstract(data);
} else {
revert("RwaUrn2/unrecognised-param");
}
emit File(what, data);
}
/**
* @notice Updates a contract parameter.
* @param what The changed parameter name. `"gemCap"
* @param data The new value of the parameter.
*/
function file(bytes32 what, uint256 data) external auth {
if (what == "gemCap") {
require(data <= 2**255 - 1, "RwaUrn2/overflow");
gemCap = data;
} else {
revert("RwaUrn2/unrecognised-param");
}
emit File(what, data);
}
/*//////////////////////////////////
Vault Operation
//////////////////////////////////*/
/**
* @notice Locks `wad` amount of the gem in the contract.
* @param wad The amount to lock.
*/
function lock(uint256 wad) external operator {
require(wad <= 2**255 - 1, "RwaUrn2/overflow");
(uint256 ink, ) = vat.urns(gemJoin.ilk(), address(this));
require(add(ink, wad) <= gemCap, "RwaUrn2/gemcap-exceeded");
DSTokenAbstract(gemJoin.gem()).transferFrom(msg.sender, address(this), wad);
// join with this contract's address
gemJoin.join(address(this), wad);
vat.frob(gemJoin.ilk(), address(this), address(this), address(this), int256(wad), 0);
emit Lock(msg.sender, wad);
}
/**
* @notice Frees `wad` amount of the gem from the contract.
* @param wad The amount to free.
*/
function free(uint256 wad) external operator {
require(wad <= 2**255, "RwaUrn2/overflow");
vat.frob(gemJoin.ilk(), address(this), address(this), address(this), -int256(wad), 0);
gemJoin.exit(msg.sender, wad);
emit Free(msg.sender, wad);
}
/**
* @notice Draws `wad` amount of Dai from the contract.
* @param wad The amount to draw.
*/
function draw(uint256 wad) external operator {
bytes32 ilk = gemJoin.ilk();
jug.drip(ilk);
(, uint256 rate, , , ) = vat.ilks(ilk);
uint256 dart = divup(rad(wad), rate);
require(dart <= 2**255 - 1, "RwaUrn2/overflow");
vat.frob(ilk, address(this), address(this), address(this), 0, int256(dart));
daiJoin.exit(outputConduit, wad);
emit Draw(msg.sender, wad);
}
/**
* @notice Repays `wad` amount of Dai to the contract.
* @param wad The amount to wipe.
*/
function wipe(uint256 wad) external {
daiJoin.join(address(this), wad);
bytes32 ilk = gemJoin.ilk();
jug.drip(ilk);
(, uint256 rate, , , ) = vat.ilks(ilk);
uint256 dart = rad(wad) / rate;
require(dart <= 2**255, "RwaUrn2/overflow");
vat.frob(ilk, address(this), address(this), address(this), 0, -int256(dart));
emit Wipe(msg.sender, wad);
}
/**
* @notice Flushes out any outstanding Dai balance to `outputConduit` address.
* @dev Can only be called after `cage()` has been called on the Vat.
*/
function quit() external {
require(vat.live() == 0, "RwaUrn2/vat-still-live");
DSTokenAbstract dai = DSTokenAbstract(daiJoin.dai());
uint256 wad = dai.balanceOf(address(this));
dai.transfer(outputConduit, wad);
emit Quit(msg.sender, wad);
}
/*//////////////////////////////////
Math
//////////////////////////////////*/
uint256 internal constant WAD = 10**18;
uint256 internal constant RAY = 10**27;
function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x + y) >= x, "DSMath/add-overflow");
}
function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x - y) <= x, "DSMath/sub-overflow");
}
function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
require(y == 0 || (z = x * y) / y == x, "DSMath/mul-overflow");
}
/**
* @dev Divides x/y, but rounds it up.
*/
function divup(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = add(x, sub(y, 1)) / y;
}
/**
* @dev Converts `wad` (10^18) into a `rad` (10^45) by multiplying it by RAY (10^27).
*/
function rad(uint256 wad) internal pure returns (uint256 z) {
return mul(wad, RAY);
}
}
// Copyright (C) 2020, 2021 Lev Livnev <lev@liv.nev.org.uk>
// Copyright (C) 2022 Dai Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.6.8 <0.7.0;
import {DSTest} from "ds-test/test.sol";
import {DSToken} from "ds-token/token.sol";
import {DSMath} from "ds-math/math.sol";
import {Vat} from "dss/vat.sol";
import {Jug} from "dss/jug.sol";
import {Spotter} from "dss/spot.sol";
import {DaiJoin} from "dss/join.sol";
import {AuthGemJoin} from "dss-gem-joins/join-auth.sol";
import {OFHTokenLike} from "./tokens/ITokenWrapper.sol";
import {TokenWrapper} from "./tokens/TokenWrapper.sol";
import {MockOFH} from "./tokens/mocks/MockOFH.sol";
import {RwaInputConduit2} from "./RwaInputConduit2.sol";
import {RwaOutputConduit2} from "./RwaOutputConduit2.sol";
import {RwaLiquidationOracle} from "./RwaLiquidationOracle.sol";
import {RwaUrn2} from "./RwaUrn2.sol";
interface Hevm {
function warp(uint256) external;
function store(
address,
bytes32,
bytes32
) external;
function load(address, bytes32) external returns (bytes32);
}
contract TokenUser {
DSToken internal immutable dai;
constructor(DSToken dai_) public {
dai = dai_;
}
function transfer(address who, uint256 wad) external {
dai.transfer(who, wad);
}
}
contract TryCaller {
function doCall(address addr, bytes memory data) external returns (bool) {
assembly {
let ok := call(gas(), addr, 0, add(data, 0x20), mload(data), 0, 0)
let free := mload(0x40)
mstore(free, ok)
mstore(0x40, add(free, 32))
revert(free, 32)
}
}
function tryCall(address addr, bytes calldata data) external returns (bool ok) {
(, bytes memory returned) = address(this).call(abi.encodeWithSignature("doCall(address,bytes)", addr, data));
ok = abi.decode(returned, (bool));
}
}
contract RwaOperator is TryCaller {
RwaUrn2 internal urn;
RwaOutputConduit2 internal outC;
RwaInputConduit2 internal inC;
constructor(
RwaUrn2 urn_,
RwaOutputConduit2 outC_,
RwaInputConduit2 inC_
) public {
urn = urn_;
outC = outC_;
inC = inC_;
}
function approve(
TokenWrapper tok,
address who,
uint256 wad
) public {
tok.approve(who, wad);
}
function pick(address who) public {
outC.pick(who);
}
function lock(uint256 wad) public {
urn.lock(wad);
}
function free(uint256 wad) public {
urn.free(wad);
}
function draw(uint256 wad) public {
urn.draw(wad);
}
function wipe(uint256 wad) public {
urn.wipe(wad);
}
function canPick(address who) public returns (bool) {
return this.tryCall(address(outC), abi.encodeWithSignature("pick(address)", who));
}
function canDraw(uint256 wad) public returns (bool) {
return this.tryCall(address(outC), abi.encodeWithSignature("draw(uint256)", wad));
}
function canFree(uint256 wad) public returns (bool) {
return this.tryCall(address(outC), abi.encodeWithSignature("free(uint256)", wad));
}
function file(bytes32 who, uint256 data) public {
urn.file(who, data);
}
}
contract RwaMate is TryCaller {
RwaOutputConduit2 internal outC;
RwaInputConduit2 internal inC;
constructor(RwaOutputConduit2 outC_, RwaInputConduit2 inC_) public {
outC = outC_;
inC = inC_;
}
function pushOut() public {
return outC.push();
}
function pushIn() public {
return inC.push();
}
function canPushOut() public returns (bool) {
return this.tryCall(address(outC), abi.encodeWithSignature("push()"));
}
function canPushIn() public returns (bool) {
return this.tryCall(address(inC), abi.encodeWithSignature("push()"));
}
}
contract RwaGov is TryCaller {
RwaUrn2 internal urn;
constructor(RwaUrn2 urn_) public {
urn = urn_;
}
function file(bytes32 who, uint256 data) public {
urn.file(who, data);
}
}
contract RwaUrn2Test is DSTest, DSMath {
bytes20 internal constant CHEAT_CODE = bytes20(uint160(uint256(keccak256("hevm cheat code"))));
Hevm internal hevm;
DSToken internal dai;
TokenWrapper internal wrapper;
MockOFH internal token;
Vat internal vat;
Jug internal jug;
Spotter internal spotter;
address internal constant VOW = address(123);
DaiJoin internal daiJoin;
AuthGemJoin internal gemJoin;
RwaLiquidationOracle internal oracle;
RwaUrn2 internal urn;
RwaOutputConduit2 internal outConduit;
RwaInputConduit2 internal inConduit;
RwaOperator internal op;
RwaMate internal mate;
TokenUser internal rec;
RwaGov internal gov;
// Debt ceiling of 1000 DAI
string internal constant DOC = "Please sign this";
uint256 internal constant CEILING = 200 ether;
uint256 internal constant EIGHT_PCT = 1000000002440418608258400030;
uint256 internal constant URN_GEM_CAP = 400 ether;
uint48 internal constant TAU = 2 weeks;
function rad(uint256 wad) internal pure returns (uint256) {
return wad * RAY;
}
function setUp() public {
hevm = Hevm(address(CHEAT_CODE));
hevm.warp(104411200);
token = new MockOFH(500);
wrapper = new TokenWrapper(address(token));
wrapper.hope(address(this));
vat = new Vat();
jug = new Jug(address(vat));
jug.file("vow", VOW);
vat.rely(address(jug));
dai = new DSToken("Dai");
daiJoin = new DaiJoin(address(vat), address(dai));
vat.rely(address(daiJoin));
dai.setOwner(address(daiJoin));
vat.init("RWA008AT1-A");
vat.file("Line", 100 * rad(CEILING));
vat.file("RWA008AT1-A", "line", rad(CEILING));
jug.init("RWA008AT1-A");
jug.file("RWA008AT1-A", "duty", EIGHT_PCT);
oracle = new RwaLiquidationOracle(address(vat), VOW);
oracle.init("RWA008AT1-A", wmul(CEILING, 1.1 ether), DOC, TAU);
vat.rely(address(oracle));
(, address pip, , ) = oracle.ilks("RWA008AT1-A");
spotter = new Spotter(address(vat));
vat.rely(address(spotter));
spotter.file("RWA008AT1-A", "mat", RAY);
spotter.file("RWA008AT1-A", "pip", pip);
spotter.poke("RWA008AT1-A");
gemJoin = new AuthGemJoin(address(vat), "RWA008AT1-A", address(wrapper));
vat.rely(address(gemJoin));
outConduit = new RwaOutputConduit2(address(dai));
urn = new RwaUrn2(
address(vat),
address(jug),
address(gemJoin),
address(daiJoin),
address(outConduit),
URN_GEM_CAP
);
gemJoin.rely(address(urn));
inConduit = new RwaInputConduit2(address(dai), address(urn));
op = new RwaOperator(urn, outConduit, inConduit);
mate = new RwaMate(outConduit, inConduit);
rec = new TokenUser(dai);
gov = new RwaGov(urn);
// Wraps all tokens into `op` balance
token.transfer(address(wrapper), 500);
wrapper.wrap(address(op), 500);
urn.hope(address(op));
urn.rely(address(gov));
inConduit.mate(address(mate));
outConduit.mate(address(mate));
outConduit.hope(address(op));
op.approve(wrapper, address(urn), type(uint256).max);
}
function testFile() public {
urn.file("outputConduit", address(123));
assertEq(urn.outputConduit(), address(123));
urn.file("jug", address(456));
assertEq(address(urn.jug()), address(456));
}
function testPickAndPush() public {
uint256 amount = 200 ether;
op.lock(amount);
op.draw(amount);
op.pick(address(rec));
mate.pushOut();
assertEq(dai.balanceOf(address(rec)), amount);
}
function testUnpickAndPickNewReceiver() public {
uint256 amount = 200 ether;
op.lock(amount);
op.draw(amount);
op.pick(address(rec));
assertTrue(mate.canPushOut());
op.pick(address(0));
assertTrue(!mate.canPushOut());
TokenUser newRec = new TokenUser(dai);
op.pick(address(newRec));
mate.pushOut();
assertEq(dai.balanceOf(address(newRec)), amount);
}
function testFailPushBeforePick() public {
uint256 amount = 200 ether;
op.lock(amount);
op.draw(amount);
mate.pushOut();
}
function testLockAndDrawFuzz(uint24 secs) public {
assertEq(dai.balanceOf(address(outConduit)), 0);
assertEq(dai.balanceOf(address(rec)), 0);
hevm.warp(block.timestamp + secs); // Let rate be > 1
assertEq(vat.dai(address(urn)), 0);
(uint256 ink, uint256 art) = vat.urns("RWA008AT1-A", address(urn));
assertEq(ink, 0);
assertEq(art, 0);
op.lock(1 ether);
op.draw(199 ether);
uint256 dustLimit = rad(15);
assertLe(vat.dai(address(urn)), dustLimit);
(, uint256 rate, , , ) = vat.ilks("RWA008AT1-A");
(ink, art) = vat.urns("RWA008AT1-A", address(urn));
assertEq(ink, 1 ether);
assertLe((art * rate) - rad(199 ether), dustLimit);
// check the amount went to the output conduit
assertEq(dai.balanceOf(address(outConduit)), 199 ether);
assertEq(dai.balanceOf(address(rec)), 0);
// op nominates the receiver
op.pick(address(rec));
// push the amount to the receiver
mate.pushOut();
assertEq(dai.balanceOf(address(outConduit)), 0);
assertEq(dai.balanceOf(address(rec)), 199 ether);
}
function testFailDrawAboveDebtCeiling() public {
op.lock(1 ether);
op.draw(1000 ether);
}
function testCannotDrawUnlessHoped() public {
op.lock(1 ether);
RwaOperator rando = new RwaOperator(urn, outConduit, inConduit);
assertTrue(!rando.canDraw(1 ether));
urn.hope(address(rando));
assertEq(dai.balanceOf(address(outConduit)), 0);
rando.draw(1 ether);
assertEq(dai.balanceOf(address(outConduit)), 1 ether);
}
function testPartialRepayment() public {
op.lock(1 ether);
op.draw(200 ether);
// op nominats the receiver
op.pick(address(rec));
mate.pushOut();
hevm.warp(block.timestamp + 30 days);
rec.transfer(address(inConduit), 100 ether);
mate.pushIn();
op.wipe(100 ether);
// Since only ~half of the loan was repaid, op cannot free the total amount locked
assertTrue(!op.canFree(1 ether));
op.free(0.4 ether);
(uint256 ink, uint256 art) = vat.urns("RWA008AT1-A", address(urn));
// 100 < art < 101 because of accumulated interest
assertLt(art - 100 ether, 1 ether);
assertEq(ink, 0.6 ether);
assertEq(dai.balanceOf(address(inConduit)), 0);
}
function testPartialRepaymentFuzz(
uint256 drawAmount,
uint256 wipeAmount,
uint256 drawTime,
uint256 wipeTime
) public {
drawAmount = (drawAmount % 150 ether) + 50 ether; // 50-200 ether
wipeAmount = wipeAmount % drawAmount; // 0-drawAmount ether
drawTime = drawTime % 15 days; // 0-15 days
wipeTime = wipeTime % 15 days; // 0-15 days
op.lock(1 ether);
hevm.warp(now + drawTime);
jug.drip("RWA008AT1-A");
op.draw(drawAmount);
op.pick(address(rec));
mate.pushOut();
hevm.warp(now + wipeTime);
jug.drip("RWA008AT1-A");
rec.transfer(address(inConduit), wipeAmount);
assertEq(dai.balanceOf(address(inConduit)), wipeAmount);
mate.pushIn();
op.wipe(wipeAmount);
}
function testRepaymentWithRoundingFuzz(
uint256 drawAmount,
uint256 drawTime,
uint256 wipeTime
) public {
drawAmount = (drawAmount % 175 ether) + 24.99 ether; // 24.99-199.99 ether
drawTime = drawTime % 15 days; // 0-15 days
wipeTime = wipeTime % 15 days; // 0-15 days
(uint256 ink, uint256 art) = vat.urns("RWA008AT1-A", address(urn));
assertEq(ink, 0);
assertEq(art, 0);
op.lock(1 ether);
hevm.warp(block.timestamp + drawTime);
jug.drip("RWA008AT1-A");
op.draw(drawAmount);
uint256 urnVatDust = vat.dai(address(urn));
// A draw should leave less than 2 RAY dust
assertLt(urnVatDust, 2 * RAY);
(, uint256 rate, , , ) = vat.ilks("RWA008AT1-A");
(ink, art) = vat.urns("RWA008AT1-A", address(urn));
assertEq(ink, 1 ether);
assertLe((art * rate) - rad(drawAmount), urnVatDust);
// op nomitates the receiver
op.pick(address(rec));
mate.pushOut();
hevm.warp(block.timestamp + wipeTime);
jug.drip("RWA008AT1-A");
(, rate, , , ) = vat.ilks("RWA008AT1-A");
uint256 fullWipeAmount = (art * rate) / RAY;
if (fullWipeAmount * RAY < art * rate) {
fullWipeAmount += 1;
}
/*/////////////////////////////////////////////////
Forcing extra DAI balance to pay accumulated fee
/////////////////////////////////////////////////*/
// Overwrite `balanceOf` for `rec` on the Dai token contract.
hevm.store(address(dai), keccak256(abi.encode(address(rec), 3)), bytes32(fullWipeAmount));
// Overwrite `totalSupply` on the Dai Token contract.
hevm.store(address(dai), bytes32(uint256(2)), bytes32(uint256(fullWipeAmount)));
// Overwite the `dai` balance mapping for `rec` on the Vat contract.
hevm.store(address(vat), keccak256(abi.encode(address(daiJoin), 5)), bytes32((fullWipeAmount * RAY)));
/*///////////////////////////////////////////////*/
rec.transfer(address(inConduit), fullWipeAmount);
assertEq(dai.balanceOf(address(inConduit)), fullWipeAmount);
mate.pushIn();
op.wipe(fullWipeAmount);
(, art) = vat.urns("RWA008AT1-A", address(urn));
assertEq(art, 0);
uint256 newUrnVatDust = vat.dai(address(urn));
assertLt(newUrnVatDust - urnVatDust, RAY);
}
function testFullRepayment() public {
op.lock(1 ether);
op.draw(200 ether);
op.pick(address(rec));
mate.pushOut();
rec.transfer(address(inConduit), 200 ether);
mate.pushIn();
RwaOperator rando = new RwaOperator(urn, outConduit, inConduit);
// authorizes `rando` on the urn
urn.hope(address(rando));
rando.wipe(200 ether);
rando.free(1 ether);
(uint256 ink, uint256 art) = vat.urns("RWA008AT1-A", address(urn));
assertEq(ink, 0);
assertEq(art, 0);
assertEq(wrapper.balanceOf(address(rando)), 1 ether);
}
function testQuit() public {
op.lock(1 ether);
op.draw(200 ether);
op.pick(address(rec));
mate.pushOut();
rec.transfer(address(inConduit), 200 ether);
mate.pushIn();
vat.cage();
assertEq(dai.balanceOf(address(urn)), 200 ether);
assertEq(dai.balanceOf(address(outConduit)), 0);
urn.quit();
assertEq(dai.balanceOf(address(urn)), 0);
assertEq(dai.balanceOf(address(outConduit)), 200 ether);
}
function testFailQuitVatStillLive() public {
op.lock(1 ether);
op.draw(200 ether);
op.pick(address(rec));
mate.pushOut();
rec.transfer(address(inConduit), 200 ether);
mate.pushIn();
urn.quit();
}
function testFailOnGemLimitExceed() public {
op.lock(URN_GEM_CAP + 1 ether);
}
function testIncreaseGemValueOnLock() public {
(uint256 ink, ) = vat.urns("RWA008AT1-A", address(urn));
uint256 gemCap = uint256(hevm.load(address(urn), bytes32(uint256(7))));
assertEq(ink, 0);
assertEq(gemCap, URN_GEM_CAP);
uint256 amount = URN_GEM_CAP;
op.lock(amount);
(uint256 inkAfter, ) = vat.urns("RWA008AT1-A", address(urn));
assertEq(inkAfter, amount);
}
function testDecreaseGemValueOnFree() public {
(uint256 ink, ) = vat.urns("RWA008AT1-A", address(urn));
assertEq(ink, 0);
op.lock(1 ether);
op.draw(200 ether);
(uint256 inkAfterDraw, ) = vat.urns("RWA008AT1-A", address(urn));
assertEq(inkAfterDraw, 1 ether);
// op nominats the receiver
op.pick(address(rec));
mate.pushOut();
hevm.warp(block.timestamp + 30 days);
rec.transfer(address(inConduit), 100 ether);
mate.pushIn();
op.wipe(100 ether);
// Since only ~half of the loan was repaid, op cannot free the total amount locked
assertTrue(!op.canFree(1 ether));
op.free(0.4 ether);
(uint256 inkAfterFree, ) = vat.urns("RWA008AT1-A", address(urn));
assertEq(inkAfterFree, 0.6 ether);
}
function testFailUnAuthorizedGemCapIncrease() public {
op.file("gemCap", 600 ether);
}
function testCanIncreaseGemCap() public {
uint256 gemCapBefore = uint256(hevm.load(address(urn), bytes32(uint256(7))));
assertEq(gemCapBefore, URN_GEM_CAP);
gov.file("gemCap", URN_GEM_CAP * 2);
uint256 gemCapAfter = uint256(hevm.load(address(urn), bytes32(uint256(7))));
assertEq(gemCapAfter, URN_GEM_CAP * 2);
}
function testCanDecreaseGemCap() public {
uint256 gemCapBefore = uint256(hevm.load(address(urn), bytes32(uint256(7))));
assertEq(gemCapBefore, URN_GEM_CAP);
gov.file("gemCap", URN_GEM_CAP / 2);
uint256 gemCapAfter = uint256(hevm.load(address(urn), bytes32(uint256(7))));
assertEq(gemCapAfter, URN_GEM_CAP / 2);
}
function testFailCannotLockMoreThanGemCapAfterDecrease() public {
gov.file("gemCap", URN_GEM_CAP / 2);
op.lock(URN_GEM_CAP);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment