diff --git a/contracts/margin/external/ERC721MarginLoan.sol b/contracts/margin/external/ERC721MarginLoan.sol new file mode 100644 index 00000000..f289c221 --- /dev/null +++ b/contracts/margin/external/ERC721MarginLoan.sol @@ -0,0 +1,314 @@ +pragma solidity 0.4.21; +pragma experimental "v0.5.0"; + +import { ReentrancyGuard } from "zeppelin-solidity/contracts/ReentrancyGuard.sol"; +import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; +import { ERC721Token } from "zeppelin-solidity/contracts/token/ERC721/ERC721Token.sol"; +import { Margin } from "../Margin.sol"; +import { TokenInteract } from "../../lib/TokenInteract.sol"; +import { ForceRecoverCollateralDelegator } from "../interfaces/ForceRecoverCollateralDelegator.sol"; +import { LoanOwner } from "../interfaces/LoanOwner.sol"; +import { MarginCallDelegator } from "../interfaces/MarginCallDelegator.sol"; + + +/** + * @title ERC721MarginLoan + * @author dYdX + * + * Contract used to tokenize margin loans as ERC721-compliant non-fungible tokens. Holding the + * token allows the holder to margin-call the position and be entitled to all payouts. Functionality + * is added to let users approve other addresses margin-call positions for them. Allows any position + * to be force-recovered by anyone as long as the payout goes to the owner of the token. + */ + /* solium-disable-next-line */ +contract ERC721MarginLoan is + ERC721Token, + MarginCallDelegator, + ForceRecoverCollateralDelegator, + ReentrancyGuard +{ + using SafeMath for uint256; + + // ============ Events ============ + + event MarginCallerApproval( + address indexed lender, + address indexed approved, + bool isApproved + ); + + event OwedTokenWithdrawn( + bytes32 indexed positionId, + address indexed lender, + address owedToken, + uint256 owedTokenWithdrawn + ); + + // ============ State Variables ============ + + // Mapping from an address to other addresses that are approved to be margin-callers + mapping (address => mapping (address => bool)) public approvedCallers; + + // Mapping from a positionId to the value of totalOwedTokensRepaidToLender for the position when + // it was initially transferred to this address. + mapping (bytes32 => uint256) public owedTokensRepaidSinceLastWithdraw; + + // Mapping from a positionId to the address of the owedToken of that position. Needed because + // margin erases this information when the position is closed. + mapping (bytes32 => address) public owedTokenForPosition; + + // ============ Constructor ============ + + function ERC721MarginLoan( + address margin + ) + public + ERC721Token("dYdX Margin Loans", "DYDX-Loan") + MarginCallDelegator(margin) + ForceRecoverCollateralDelegator(margin) + { + } + + // ============ Token-Holder functions ============ + + /** + * Approves any address to margin-call any of the positions owned by the sender. + * + * @param caller Address of the margin-caller + * @param isApproved True if approving the caller, false if revoking approval + */ + function approveCaller( + address caller, + bool isApproved + ) + external + nonReentrant + { + // cannot approve self since any address can already close its own positions + require(caller != msg.sender); + + if (approvedClosers[msg.sender][caller] != isApproved) { + approvedClosers[msg.sender][caller] = isApproved; + emit CallerApproval( + msg.sender, + caller, + isApproved + ); + } + } + + /** + * Transfer ownership of the loan externally to this contract, thereby burning the token. + * + * @param positionId Unique ID of the position + * @param to Address to transfer loan ownership to + * @param safely If true, requires that there is no owedToken held in this contract for + * the loan being transferred. Once the loan is untokenized, there is no way + * for the lender to withdraw the tokens from this contract. + */ + function untokenizeLoan( + bytes32 positionId, + address to, + bool safely + ) + external + nonReentrant + { + uint256 tokenId = uint256(positionId); + require(msg.sender == ownerOf(tokenId)); + + if (safely) { + uint256 totalRepaid = Margin(DYDX_MARGIN).getTotalOwedTokensRepaidToLender(positionId); + require(totalRepaid == owedTokensRepaidSinceLastWithdraw[positionId]); + } + + _burn(msg.sender, tokenId); // requires msg.sender to be owner + Margin(DYDX_MARGIN).transferLoan(positionId, to); + } + + /** + * Helper to allow withdrawal for multiple positions in one call + * + * @param who Array of positions to withdraw for + */ + function withdrawMultiple( + bytes32[] positionIds + ) + external + nonReentrant + { + for (uint256 i = 0; i < positionIds.length; i++) { + withdrawImpl(positionIds[i]); + } + } + + /** + * Withdraw the owedToken repaid for a loan + * + * @param positionId Unique ID of the position + * @return The amount of owedToken withdrawn + */ + function withdraw( + bytes32 positionId + ) + external + nonReentrant + returns (uint256) + { + return withdrawImpl(positionId); + } + + // ============ OnlyMargin Functions ============ + + /** + * Called by the Margin contract when anyone transfers ownership of a position to this contract. + * This function mints a new ERC721 Token and returns this address to + * indicate to Margin that it is willing to take ownership of the position. + * + * @param from Address of previous position owner + * @param positionId Unique ID of the position + * @return This address on success, throw otherwise + */ + function receiveLoanOwnership( + address from, + bytes32 positionId + ) + external + onlyMargin + nonReentrant + returns (address) + { + _mint(from, uint256(positionId)); + + owedTokensRepaidSinceLastWithdraw[positionId] = + Margin(DYDX_MARGIN).getTotalOwedTokensRepaidToLender(positionId); + owedTokenForPosition[positionId] = + Margin(DYDX_MARGIN).getPositionOwedToken(positionId); + + return address(this); // returning own address retains ownership of position + } + + /** + * Called by Margin when additional value is added onto a position. Rejects this addition. + * + * param from Address that added the value to the position + * param positionId Unique ID of the position + * param principalAdded Principal amount added to position + * @return False + */ + function marginLoanIncreased( + address, /* from */ + bytes32, /* positionId */ + uint256 /* principalAdded */ + ) + external + onlyMargin + nonReentrant + returns (bool) + { + return false; + } + + /** + * Called by Margin when another address attempts to margin-call a loan + * + * @param who Address attempting to initiate the loan call + * @param positionId Unique ID of the position + * param depositAmount (unused) + * @return True to consent to the loan being called if the initiator is a trusted + * loan caller or the owner of the loan + */ + function marginCallOnBehalfOf( + address who, + bytes32 positionId, + uint256 /* depositAmount */ + ) + external + onlyMargin + nonReentrant + returns (bool) + { + address owner = ownerOf(uint256(positionId)); + return (who == owner) || approvedCallers[owner][who]; + } + + /** + * Called by Margin when another address attempts to cancel a margin call for a loan + * + * @param who Address attempting to initiate the loan call cancel + * @param positionId Unique ID of the position + * @return True to consent to the loan call being canceled if the initiator is a + * trusted loan caller or the owner of the loan + */ + function cancelMarginCallOnBehalfOf( + address who, + bytes32 positionId + ) + external + onlyMargin + nonReentrant + returns (bool) + { + address owner = ownerOf(uint256(positionId)); + return (who == owner) || approvedCallers[owner][who]; + } + + /** + * Called by Margin when another address attempts to force recover the loan. Allow anyone to + * force recover the loan as long as the payout goes to the token owner. + * + * param (unused) + * @param positionId Unique ID of the position + * @param collateralRecipient Address to send the recovered tokens to + * @return True if forceRecoverCollateral() is permitted + */ + function forceRecoverCollateralOnBehalfOf( + address /* who */, + bytes32 positionId, + address collateralRecipient + ) + external + onlyMargin + nonReentrant + returns (bool) + { + return ownerOf(uint256(positionId)) == collateralRecipient; + } + + // ============ Helper Functions ============ + + /** + * Implementation of withdrawing owedToken for a particular positionId + * + * @param positionId Unique ID of the position + * @return The number of owedToken withdrawn + */ + function withdrawImpl( + bytes32 positionId + ) + internal + returns (uint256) + { + uint256 totalRepaid = Margin(DYDX_MARGIN).getTotalOwedTokensRepaidToLender(positionId); + uint256 tokensToSend = totalRepaid.sub(owedTokensRepaidSinceLastWithdraw[positionId]); + + if (tokensToSend == 0) { + return 0; + } + + owedTokensRepaidSinceLastWithdraw[positionId] = totalRepaid; + + address owedToken = owedTokenForPosition[positionId]; + address owner = ownerOf(uint256(positionId)); + TokenInteract.transfer(owedToken, owner, tokensToSend); + + emit OwedTokenWithdrawn( + positionId, + owner, + owedToken, + tokensToSend + ); + + return tokensToSend; + } +} diff --git a/contracts/margin/external/ERC721MarginPosition.sol b/contracts/margin/external/ERC721MarginPosition.sol index 9e793c5f..ff4efd8d 100644 --- a/contracts/margin/external/ERC721MarginPosition.sol +++ b/contracts/margin/external/ERC721MarginPosition.sol @@ -54,7 +54,7 @@ contract ERC721MarginPosition is address margin ) public - ERC721Token("dYdX Margin Positions", "DYDX-M") + ERC721Token("dYdX Margin Positions", "DYDX-MarginPosition") ClosePositionDelegator(margin) { } @@ -112,7 +112,7 @@ contract ERC721MarginPosition is * @param positionId Unique ID of the position * @param to Address to transfer postion ownership to */ - function transferPosition( + function untokenizePosition( bytes32 positionId, address to ) diff --git a/contracts/margin/external/SharedLoan.sol b/contracts/margin/external/SharedLoan.sol index 620a1630..2a536e68 100644 --- a/contracts/margin/external/SharedLoan.sol +++ b/contracts/margin/external/SharedLoan.sol @@ -7,8 +7,8 @@ import { Margin } from "../Margin.sol"; import { MathHelpers } from "../../lib/MathHelpers.sol"; import { TokenInteract } from "../../lib/TokenInteract.sol"; import { MarginCommon } from "../impl/MarginCommon.sol"; -import { CallLoanDelegator } from "../interfaces/CallLoanDelegator.sol"; import { ForceRecoverCollateralDelegator } from "../interfaces/ForceRecoverCollateralDelegator.sol"; +import { MarginCallDelegator } from "../interfaces/MarginCallDelegator.sol"; import { MarginHelper } from "./lib/MarginHelper.sol"; @@ -22,7 +22,7 @@ import { MarginHelper } from "./lib/MarginHelper.sol"; */ /* solium-disable-next-line */ contract SharedLoan is - CallLoanDelegator, + MarginCallDelegator, ForceRecoverCollateralDelegator, ReentrancyGuard { @@ -109,7 +109,7 @@ contract SharedLoan is ) public ForceRecoverCollateralDelegator(margin) - CallLoanDelegator(margin) + MarginCallDelegator(margin) { POSITION_ID = positionId; state = State.UNINITIALIZED; @@ -168,15 +168,15 @@ contract SharedLoan is * Called by Margin when additional value is added onto the position this contract * is lending for. Balance is added to the address that loaned the additional tokens. * - * @param from Address that loaned the additional tokens - * @param positionId Unique ID of the position - * @param amountAdded Amount that was added to the position - * @return True to indicate that this contract consents to value being added + * @param from Address that loaned the additional tokens + * @param positionId Unique ID of the position + * @param principalAdded Amount that was added to the position + * @return True to indicate that this contract consents to value being added */ function marginLoanIncreased( address from, bytes32 positionId, - uint256 amountAdded + uint256 principalAdded ) external onlyMargin diff --git a/contracts/margin/impl/LoanImpl.sol b/contracts/margin/impl/LoanImpl.sol index de310c76..7147fc18 100644 --- a/contracts/margin/impl/LoanImpl.sol +++ b/contracts/margin/impl/LoanImpl.sol @@ -5,7 +5,7 @@ import { Math } from "zeppelin-solidity/contracts/math/Math.sol"; import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; import { MarginCommon } from "./MarginCommon.sol"; import { MarginState } from "./MarginState.sol"; -import { CallLoanDelegator } from "../interfaces/CallLoanDelegator.sol"; +import { MarginCallDelegator } from "../interfaces/MarginCallDelegator.sol"; /** @@ -79,7 +79,7 @@ library LoanImpl { // If not the lender, requires the lender to approve msg.sender if (msg.sender != position.lender) { require( - CallLoanDelegator(position.lender).marginCallOnBehalfOf( + MarginCallDelegator(position.lender).marginCallOnBehalfOf( msg.sender, positionId, requiredDeposit @@ -116,7 +116,7 @@ library LoanImpl { // If not the lender, requires the lender to approve msg.sender if (msg.sender != position.lender) { require( - CallLoanDelegator(position.lender).cancelMarginCallOnBehalfOf( + MarginCallDelegator(position.lender).cancelMarginCallOnBehalfOf( msg.sender, positionId ) diff --git a/contracts/margin/interfaces/LoanOwner.sol b/contracts/margin/interfaces/LoanOwner.sol index 1ee55e0f..2790d263 100644 --- a/contracts/margin/interfaces/LoanOwner.sol +++ b/contracts/margin/interfaces/LoanOwner.sol @@ -47,16 +47,16 @@ contract LoanOwner is OnlyMargin { * Margin#increasePosition. If true is returned, the implementing contract can assume * the additional value was added. * - * @param from Lender adding additional funds to the position - * @param positionId Unique ID of the position - * @param amountAdded Amount to be added to the position - * @return True if the contract consents to additional value being added, - * false otherwise + * @param from Lender adding additional funds to the position + * @param positionId Unique ID of the position + * @param principalAdded Principal amount to be added to the position + * @return True if the contract consents to additional value being added, + * false otherwise */ function marginLoanIncreased( address from, bytes32 positionId, - uint256 amountAdded + uint256 principalAdded ) external onlyMargin diff --git a/contracts/margin/interfaces/CallLoanDelegator.sol b/contracts/margin/interfaces/MarginCallDelegator.sol similarity index 93% rename from contracts/margin/interfaces/CallLoanDelegator.sol rename to contracts/margin/interfaces/MarginCallDelegator.sol index d7df142f..ec0106ef 100644 --- a/contracts/margin/interfaces/CallLoanDelegator.sol +++ b/contracts/margin/interfaces/MarginCallDelegator.sol @@ -5,17 +5,17 @@ import { LoanOwner } from "./LoanOwner.sol"; /** - * @title CallLoanDelegator + * @title MarginCallDelegator * @author dYdX * - * Interface that smart contracts must implement in order to let other addresses call-in a loan + * Interface that smart contracts must implement in order to let other addresses margin-call a loan * owned by the smart contract. */ -contract CallLoanDelegator is LoanOwner { +contract MarginCallDelegator is LoanOwner { // ============ Constructor ============ - function CallLoanDelegator( + function MarginCallDelegator( address margin ) public diff --git a/contracts/testing/TestLoanOwner.sol b/contracts/testing/TestLoanOwner.sol index 225a2403..f38db37b 100644 --- a/contracts/testing/TestLoanOwner.sol +++ b/contracts/testing/TestLoanOwner.sol @@ -46,7 +46,7 @@ contract TestLoanOwner is LoanOwner { function marginLoanIncreased( address from, bytes32 positionId, - uint256 amount + uint256 principalAdded ) onlyMargin external diff --git a/contracts/testing/TestMarginCallDelegator.sol b/contracts/testing/TestMarginCallDelegator.sol index 1f7ce9b6..1e029cab 100644 --- a/contracts/testing/TestMarginCallDelegator.sol +++ b/contracts/testing/TestMarginCallDelegator.sol @@ -1,21 +1,21 @@ pragma solidity 0.4.21; pragma experimental "v0.5.0"; -import { CallLoanDelegator } from "../margin/interfaces/CallLoanDelegator.sol"; +import { MarginCallDelegator } from "../margin/interfaces/MarginCallDelegator.sol"; -contract TestCallLoanDelegator is CallLoanDelegator { +contract TestMarginCallDelegator is MarginCallDelegator { address public CALLER; address public CANCELLER; - function TestCallLoanDelegator( + function TestMarginCallDelegator( address margin, address caller, address canceller ) public - CallLoanDelegator(margin) + MarginCallDelegator(margin) { CALLER = caller; CANCELLER = canceller; diff --git a/test/margin/TestMarginCall.js b/test/margin/TestMarginCall.js index 366eb68f..ba11a3ef 100644 --- a/test/margin/TestMarginCall.js +++ b/test/margin/TestMarginCall.js @@ -6,7 +6,7 @@ chai.use(require('chai-bignumber')()); const BigNumber = require('bignumber.js'); const Margin = artifacts.require("Margin"); -const TestCallLoanDelegator = artifacts.require("TestCallLoanDelegator"); +const TestMarginCallDelegator = artifacts.require("TestMarginCallDelegator"); const { doOpenPosition, getPosition, @@ -88,11 +88,11 @@ describe('#marginCall', () => { }); contract('Margin', function(accounts) { - it('CallLoanDelegator loan owner only allows certain accounts', async () => { + it('MarginCallDelegator loan owner only allows certain accounts', async () => { dydxMargin = await Margin.deployed(); const OpenTx = await doOpenPosition(accounts); const caller = accounts[8]; - const loanCaller = await TestCallLoanDelegator.new( + const loanCaller = await TestMarginCallDelegator.new( Margin.address, caller, ADDRESSES.ZERO); @@ -211,11 +211,11 @@ describe('#cancelMarginCall', () => { }); contract('Margin', function(accounts) { - it('CallLoanDelegator loan owner only allows certain accounts', async () => { + it('MarginCallDelegator loan owner only allows certain accounts', async () => { dydxMargin = await Margin.deployed(); const { OpenTx } = await doOpenPositionAndCall(accounts); const canceller = accounts[9]; - const loanCaller = await TestCallLoanDelegator.new( + const loanCaller = await TestMarginCallDelegator.new( Margin.address, ADDRESSES.ZERO, canceller); diff --git a/test/margin/TestOpenPosition.js b/test/margin/TestOpenPosition.js index 05fc501b..eb0e5e00 100644 --- a/test/margin/TestOpenPosition.js +++ b/test/margin/TestOpenPosition.js @@ -12,7 +12,7 @@ const FeeToken = artifacts.require("TokenC"); const Vault = artifacts.require("Vault"); const ProxyContract = artifacts.require("Proxy"); const TestSmartContractLender = artifacts.require("TestSmartContractLender"); -const TestCallLoanDelegator = artifacts.require("TestCallLoanDelegator"); +const TestMarginCallDelegator = artifacts.require("TestMarginCallDelegator"); const TestLoanOwner = artifacts.require("TestLoanOwner"); const TestClosePositionDelegator = artifacts.require("TestClosePositionDelegator"); const TestPositionOwner = artifacts.require("TestPositionOwner"); @@ -114,14 +114,14 @@ describe('#openPosition', () => { lenderOwedTokenBalance ) ]); - const testCallLoanDelegator = await TestCallLoanDelegator.new( + const testMarginCallDelegator = await TestMarginCallDelegator.new( Margin.address, ADDRESSES.ZERO, ADDRESSES.ZERO); OpenTx.loanOffering.signer = OpenTx.loanOffering.payer; OpenTx.loanOffering.payer = testSmartContractLender.address; - OpenTx.loanOffering.owner = testCallLoanDelegator.address; + OpenTx.loanOffering.owner = testMarginCallDelegator.address; OpenTx.loanOffering.signature = await signLoanOffering(OpenTx.loanOffering); const tx = await callOpenPosition(dydxMargin, OpenTx); @@ -168,7 +168,7 @@ describe('#openPosition', () => { contract('Margin', function(accounts) { it('properly assigns owner for lender and owner for contracts', async () => { const dydxMargin = await Margin.deployed(); - const testCallLoanDelegator = await TestCallLoanDelegator.new( + const testMarginCallDelegator = await TestMarginCallDelegator.new( Margin.address, ADDRESSES.ZERO, ADDRESSES.ZERO); @@ -179,7 +179,7 @@ describe('#openPosition', () => { const OpenTx = await createOpenTx(accounts); await issueTokensAndSetAllowances(OpenTx); OpenTx.owner = testClosePositionDelegator.address; - OpenTx.loanOffering.owner = testCallLoanDelegator.address; + OpenTx.loanOffering.owner = testMarginCallDelegator.address; OpenTx.loanOffering.signature = await signLoanOffering(OpenTx.loanOffering); await callOpenPosition(dydxMargin, OpenTx); await checkSuccess(dydxMargin, OpenTx); @@ -189,7 +189,7 @@ describe('#openPosition', () => { contract('Margin', function(accounts) { it('properly assigns owner for lender and owner for chaining', async () => { const dydxMargin = await Margin.deployed(); - const testCallLoanDelegator = await TestCallLoanDelegator.new( + const testMarginCallDelegator = await TestMarginCallDelegator.new( Margin.address, ADDRESSES.ZERO, ADDRESSES.ZERO); @@ -199,7 +199,7 @@ describe('#openPosition', () => { false); const testLoanOwner = await TestLoanOwner.new( Margin.address, - testCallLoanDelegator.address, + testMarginCallDelegator.address, false); const testPositionOwner = await TestPositionOwner.new( Margin.address, diff --git a/test/margin/TestTransfer.js b/test/margin/TestTransfer.js index 2308f53e..a1c0163c 100644 --- a/test/margin/TestTransfer.js +++ b/test/margin/TestTransfer.js @@ -6,7 +6,7 @@ const TokenA = artifacts.require("TokenA"); const Margin = artifacts.require("Margin"); const TestClosePositionDelegator = artifacts.require("TestClosePositionDelegator"); const TestPositionOwner = artifacts.require("TestPositionOwner"); -const TestCallLoanDelegator = artifacts.require("TestCallLoanDelegator"); +const TestMarginCallDelegator = artifacts.require("TestMarginCallDelegator"); const TestLoanOwner = artifacts.require("TestLoanOwner"); const { doOpenPosition, @@ -240,15 +240,15 @@ describe('#transferLoan', () => { it('successfully transfers to a contract with the correct interface', async () => { dydxMargin = await Margin.deployed(); OpenTx = await doOpenPosition(accounts); - const testCallLoanDelegator = await TestCallLoanDelegator.new( + const testMarginCallDelegator = await TestMarginCallDelegator.new( dydxMargin.address, ADDRESSES.ZERO, ADDRESSES.ZERO); const tx = - await transferLoan(OpenTx, testCallLoanDelegator.address, OpenTx.loanOffering.payer); + await transferLoan(OpenTx, testMarginCallDelegator.address, OpenTx.loanOffering.payer); const { lender } = await getPosition(dydxMargin, OpenTx.id); - expect(lender.toLowerCase()).to.eq(testCallLoanDelegator.address.toLowerCase()); + expect(lender.toLowerCase()).to.eq(testMarginCallDelegator.address.toLowerCase()); console.log('\tMargin.transferLoan gas used (to contract): ' + tx.receipt.gasUsed); }); }); @@ -257,20 +257,20 @@ describe('#transferLoan', () => { it('successfully transfers to a contract that chains to another contract', async () => { dydxMargin = await Margin.deployed(); OpenTx = await doOpenPosition(accounts); - const testCallLoanDelegator = await TestCallLoanDelegator.new( + const testMarginCallDelegator = await TestMarginCallDelegator.new( dydxMargin.address, ADDRESSES.ZERO, ADDRESSES.ZERO); const testLoanOwner = await TestLoanOwner.new( dydxMargin.address, - testCallLoanDelegator.address, + testMarginCallDelegator.address, false); const tx = await transferLoan( OpenTx, testLoanOwner.address, OpenTx.loanOffering.payer, - testCallLoanDelegator.address); + testMarginCallDelegator.address); console.log('\tMargin.transferLoan gas used (chain thru): ' + tx.receipt.gasUsed); }); }); diff --git a/test/margin/external/TestERC721MarginPosition.js b/test/margin/external/TestERC721MarginPosition.js index 46d89cda..3b472b3c 100644 --- a/test/margin/external/TestERC721MarginPosition.js +++ b/test/margin/external/TestERC721MarginPosition.js @@ -168,7 +168,7 @@ contract('ERC721MarginPosition', function(accounts) { }); }); - describe('#transferPosition', () => { + describe('#untokenizePosition', () => { const receiver = accounts[9]; const trader = accounts[0]; let OpenTx; @@ -180,7 +180,7 @@ contract('ERC721MarginPosition', function(accounts) { }); it('succeeds when called by ownerOf', async () => { - await erc721Contract.transferPosition(OpenTx.id, receiver, { from: trader }); + await erc721Contract.untokenizePosition(OpenTx.id, receiver, { from: trader }); await expectThrow(erc721Contract.ownerOf.call(uint256(OpenTx.id))); const newOwner = await dydxMargin.getPositionOwner.call(OpenTx.id); expect(newOwner).to.equal(receiver); @@ -188,12 +188,12 @@ contract('ERC721MarginPosition', function(accounts) { it('fails for a non-owner', async () => { await expectThrow( - erc721Contract.transferPosition(OpenTx.id, receiver, { from: accounts[2] })); + erc721Contract.untokenizePosition(OpenTx.id, receiver, { from: accounts[2] })); }); it('fails for a non-existant position', async () => { await expectThrow( - erc721Contract.transferPosition(BYTES32.BAD_ID, receiver, { from: trader })); + erc721Contract.untokenizePosition(BYTES32.BAD_ID, receiver, { from: trader })); }); });