Last active
March 22, 2022 20:07
-
-
Save hbarcelos/5e95237114fed2471dbe26d9a318dec8 to your computer and use it in GitHub Desktop.
RwaLiquidationOracle2 and RwaUrn2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (C) 2020, 2021 Lev Livnev <[email protected]> | |
// 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 <[email protected]> | |
* @author Henrique Barcelos <[email protected]> | |
* @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"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (C) 2020, 2021 Lev Livnev <[email protected]> | |
// 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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (C) 2020, 2021 Lev Livnev <[email protected]> | |
// 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 <[email protected]> | |
* @author Kaue Cano <[email protected]> | |
* @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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (C) 2020, 2021 Lev Livnev <[email protected]> | |
// 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